@heyanon-arp/cli 0.0.5 → 0.0.7
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.
- package/README.md +18 -23
- package/dist/cli.js +4313 -5017
- package/dist/cli.js.map +1 -1
- package/examples/README.md +40 -30
- package/examples/worker-template.py +149 -209
- package/package.json +6 -3
- package/scripts/postinstall.mjs +54 -0
package/examples/README.md
CHANGED
|
@@ -19,14 +19,14 @@ network access or a checkout of the source repository — `npm i -g
|
|
|
19
19
|
|
|
20
20
|
| File | Subject | Status |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| [`worker-template.py`](./worker-template.py) | Autonomous Python worker (handshake →
|
|
22
|
+
| [`worker-template.py`](./worker-template.py) | Autonomous Python worker (handshake → delegation → work → receipt → settlement, all auto-mediated; terms ride inline on the offer, you fill in `handle_work_request()`) | Reference; FLAT pricing + full-release settlement only. MIT. |
|
|
23
23
|
|
|
24
24
|
## How to use the worker template
|
|
25
25
|
|
|
26
26
|
1. Register an agent (one-time):
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
heyarp register --name "MyWorker" --tag my-service
|
|
29
|
+
heyarp register --name "MyWorker" --tag my-service
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
2. Copy the template:
|
|
@@ -50,10 +50,10 @@ network access or a checkout of the source repository — `npm i -g
|
|
|
50
50
|
The worker refuses to start while the placeholder is still in place
|
|
51
51
|
(so funds can never accidentally route to the template's default).
|
|
52
52
|
It also refuses if the value does not match your agent's actual
|
|
53
|
-
settlement key: `
|
|
54
|
-
agent's stored key, so a mismatch would route funds to the
|
|
55
|
-
address rather than the one you set here. The preflight
|
|
56
|
-
real key via `heyarp whoami --local` and compares.
|
|
53
|
+
settlement key: the `receipt propose` escrow settlement step signs
|
|
54
|
+
with the agent's stored key, so a mismatch would route funds to the
|
|
55
|
+
agent's address rather than the one you set here. The preflight
|
|
56
|
+
resolves the real key via `heyarp whoami --local` and compares.
|
|
57
57
|
|
|
58
58
|
5. Pick your cluster. Defaults to **mainnet-beta** (cluster tag `1`).
|
|
59
59
|
Override for devnet testing:
|
|
@@ -88,29 +88,39 @@ Docker / launchd).
|
|
|
88
88
|
|
|
89
89
|
## Caveats
|
|
90
90
|
|
|
91
|
-
- **Pricing model coverage — FLAT by the template's choice.**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
91
|
+
- **Pricing model coverage — FLAT by the template's choice.** Terms
|
|
92
|
+
ride inline on the delegation offer (there is no separate contract
|
|
93
|
+
step), so the template's `auto_accept_delegation` reads the offer's
|
|
94
|
+
`pricingModel` straight off the delegation row and refuses non-flat
|
|
95
|
+
offers (a full-release signature on a `usage_based` lock would let
|
|
96
|
+
the buyer drain it). The settlement step folded into `receipt
|
|
97
|
+
propose` (escrow) DOES handle both: it auto-detects full (flat) vs
|
|
98
|
+
partial (`usage_based`, from the receipt's `usage.computed_amount`)
|
|
99
|
+
release and signs the right digest (`ARP-SOLANA-RELEASE-v1.5` /
|
|
100
|
+
`ARP-SOLANA-PARTIAL-RELEASE-v1.5`). To support `usage_based` work,
|
|
101
|
+
TWO changes are needed — not just one: (1) relax the FLAT-only gate
|
|
102
|
+
in `auto_accept_delegation`, AND (2) compute the per-task amount and
|
|
103
|
+
pass `--computed-amount` on the `receipt propose` call (the folded
|
|
104
|
+
settlement step binds the partial release to the receipt's
|
|
105
|
+
`usage.computed_amount` and refuses to settle a usage_based receipt
|
|
106
|
+
that lacks it).
|
|
107
|
+
- **Settlement is folded into `receipt propose` now.** For an escrow
|
|
108
|
+
delegation the ~90-line payee settlement ritual (resolve buyer key →
|
|
109
|
+
derive condition_hash → fetch amount/decimals/deadline → compute
|
|
110
|
+
expiry → sign → deliver) runs automatically as part of `heyarp
|
|
111
|
+
receipt propose ... --cluster-tag <0|1>`, right after the receipt
|
|
112
|
+
commits — so a worker can't forget the separate step (which would
|
|
113
|
+
strand the buyer's cosign on `ESC_SETTLEMENT_SIGS_MISSING`). It
|
|
114
|
+
auto-resolves the rest from the delegation id, signs the release
|
|
115
|
+
digest (reusing `wallet sign-settlement-release`), and delivers via a
|
|
116
|
+
`settlement_signature` envelope the buyer consumes with `receipt
|
|
117
|
+
cosign --auto-resolve-payee-sig`. Idempotent — safe to re-run.
|
|
118
|
+
`--cluster-tag` is REQUIRED for escrow (propose fails fast before
|
|
119
|
+
committing if it's missing). On partial failure (no RPC / lock not
|
|
120
|
+
yet visible) the receipt stays committed and the `--json` result's
|
|
121
|
+
`settlement` block reports `{delivered: false, error, recovery}`;
|
|
122
|
+
recover with `heyarp receipt send-payee-sig` (the no-RPC primitive)
|
|
123
|
+
or re-run propose once the lock is reachable.
|
|
114
124
|
- **Policy is hard-coded.** The auto-accept criteria live inline in
|
|
115
125
|
the Python file. A future `heyarp worker run --policy worker-policy.yaml`
|
|
116
126
|
worker daemon would move those into a
|
|
@@ -118,7 +128,7 @@ Docker / launchd).
|
|
|
118
128
|
Until that ships, treat this template as a quick way to validate
|
|
119
129
|
the protocol against your business logic, not as a production
|
|
120
130
|
runner — at minimum, add a price ceiling check in
|
|
121
|
-
`
|
|
131
|
+
`auto_accept_delegation()` before accepting.
|
|
122
132
|
- **Single-tenant.** Events are processed serially in the SSE loop;
|
|
123
133
|
if you need parallelism, run multiple workers under **distinct DIDs**
|
|
124
134
|
(running two against the same identity will fight over
|
|
@@ -4,16 +4,15 @@ ARP Worker Template
|
|
|
4
4
|
===================
|
|
5
5
|
Plug-and-play template for building autonomous ARP (Agent Relationship
|
|
6
6
|
Protocol) workers. Drop your business logic into `handle_work_request()`
|
|
7
|
-
— everything else (handshake →
|
|
8
|
-
|
|
7
|
+
— everything else (handshake → delegation → work → receipt →
|
|
8
|
+
settlement) is auto-mediated.
|
|
9
9
|
|
|
10
10
|
What this template does:
|
|
11
11
|
handshake → auto-accept
|
|
12
|
-
|
|
13
|
-
delegation offer → auto-accept (idempotent, escrow-aware)
|
|
12
|
+
delegation offer → auto-accept (terms inline, idempotent, escrow-aware)
|
|
14
13
|
work_request → YOUR CUSTOM HANDLER → work_response
|
|
15
|
-
→ auto receipt propose
|
|
16
|
-
|
|
14
|
+
→ auto receipt propose (for escrow, the SAME command
|
|
15
|
+
also signs + delivers the payee settlement signature)
|
|
17
16
|
|
|
18
17
|
Quick start:
|
|
19
18
|
1. Register your agent: heyarp register --name "MyWorker" --tag my-service ...
|
|
@@ -29,13 +28,15 @@ Requirements:
|
|
|
29
28
|
- Registered ARP agent identity
|
|
30
29
|
|
|
31
30
|
Scope and limitations (V1 alpha):
|
|
32
|
-
- **FLAT pricing only — by THIS template's choice.** `
|
|
33
|
-
below refuses to
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
- **FLAT pricing only — by THIS template's choice.** `auto_accept_delegation`
|
|
32
|
+
below refuses to accept non-flat offers (the delegation carries its
|
|
33
|
+
pricing_model inline now; a full-release signature on a usage_based
|
|
34
|
+
lock would let the buyer drain it). The folded settlement step in
|
|
35
|
+
`heyarp receipt propose` (escrow) DOES handle partial release (it
|
|
36
|
+
auto-detects from the receipt's usage.computed_amount), so to support
|
|
37
|
+
usage_based work, relax the pricing gate in `auto_accept_delegation`
|
|
38
|
+
AND pass `--computed-amount` on the propose call — the settlement step
|
|
39
|
+
then signs the right (partial) digest.
|
|
39
40
|
- **Native SOL only by default.** SPL-token settlement requires
|
|
40
41
|
setting `ARP_MINT_PUBKEY` to the token's mint address (passed through
|
|
41
42
|
to the settlement command).
|
|
@@ -140,8 +141,8 @@ _SETTLEMENT_SENTINEL = "REPLACE_WITH_YOUR_SETTLEMENT_PUBKEY"
|
|
|
140
141
|
MY_SETTLEMENT_PUBKEY = os.environ.get("ARP_SETTLEMENT_PUBKEY", _SETTLEMENT_SENTINEL)
|
|
141
142
|
|
|
142
143
|
# Cluster tag baked into the on-chain settlement digest. Must match
|
|
143
|
-
# the cluster the
|
|
144
|
-
# `release_lock` rejects the signature.
|
|
144
|
+
# the cluster the escrow program's `create_lock` was submitted on,
|
|
145
|
+
# otherwise `release_lock` rejects the signature.
|
|
145
146
|
# 0 = devnet
|
|
146
147
|
# 1 = mainnet-beta
|
|
147
148
|
# Default is `1` (mainnet) so a production worker is safe by default;
|
|
@@ -229,9 +230,9 @@ def error_code(r: dict) -> str | None:
|
|
|
229
230
|
# FSM AUTO-RESPONDERS — handle the protocol state machine
|
|
230
231
|
# ═══════════════════════════════════════════════════════════════
|
|
231
232
|
#
|
|
232
|
-
# The ARP work cycle is: handshake →
|
|
233
|
-
#
|
|
234
|
-
#
|
|
233
|
+
# The ARP work cycle is: handshake → delegation → work → receipt.
|
|
234
|
+
# These handlers auto-accept every non-terminal phase so your worker
|
|
235
|
+
# runs without human intervention.
|
|
235
236
|
#
|
|
236
237
|
# All acceptors are IDEMPOTENT — they safely skip already-processed
|
|
237
238
|
# states (SSE re-delivers events, so retries are normal).
|
|
@@ -240,14 +241,14 @@ def auto_accept_handshake(sender_did: str):
|
|
|
240
241
|
"""
|
|
241
242
|
Auto-accept an inbound handshake.
|
|
242
243
|
|
|
243
|
-
Accepting establishes the relationship and unlocks
|
|
244
|
-
|
|
244
|
+
Accepting establishes the relationship and unlocks the delegation
|
|
245
|
+
offer from the buyer.
|
|
245
246
|
"""
|
|
246
247
|
print(f"[worker] Auto-accepting handshake from {sender_did[:30]}...")
|
|
247
248
|
r = shell([
|
|
248
249
|
HEYARP, "send-handshake-response", sender_did,
|
|
249
250
|
"--decision", "accept",
|
|
250
|
-
"--notes", "Auto-accepted. Ready for work. Send
|
|
251
|
+
"--notes", "Auto-accepted. Ready for work. Send a delegation offer.",
|
|
251
252
|
"--json",
|
|
252
253
|
], timeout=30)
|
|
253
254
|
if r["ok"]:
|
|
@@ -289,133 +290,38 @@ def fetch_delegation_row(rel_id: str, delegation_id: str) -> dict | None:
|
|
|
289
290
|
return None
|
|
290
291
|
|
|
291
292
|
|
|
292
|
-
def fetch_contract_row(rel_id: str, contract_id: str) -> dict | None:
|
|
293
|
-
"""
|
|
294
|
-
Look up the latest-version row for `contract_id` and return it
|
|
295
|
-
(dict with `state`, `pricingModel`, `version`, ...). Returns None
|
|
296
|
-
if the row is missing or the server rejects the list call.
|
|
297
|
-
|
|
298
|
-
Paginates through `contracts --json` with the maximum page size
|
|
299
|
-
(100). The CLI default is 20, which would miss a contract on a
|
|
300
|
-
long-lived relationship — this loop walks until pages exhaust,
|
|
301
|
-
bounded at 50 pages × 100 rows = 5000 contracts (any real-world
|
|
302
|
-
relationship beyond that is anomalous and worth manual inspection).
|
|
303
|
-
"""
|
|
304
|
-
matches = []
|
|
305
|
-
after = None
|
|
306
|
-
for _ in range(50):
|
|
307
|
-
cmd = [HEYARP, "contracts", rel_id, "--json", "--limit", "100"]
|
|
308
|
-
if after:
|
|
309
|
-
cmd += ["--after", after]
|
|
310
|
-
rows = shell_json(cmd, timeout=15)
|
|
311
|
-
if rows is None:
|
|
312
|
-
return None
|
|
313
|
-
if not rows:
|
|
314
|
-
break
|
|
315
|
-
matches.extend(r for r in rows if r.get("contractId") == contract_id)
|
|
316
|
-
if len(rows) < 100:
|
|
317
|
-
break
|
|
318
|
-
after = rows[-1].get("id")
|
|
319
|
-
if not matches:
|
|
320
|
-
return None
|
|
321
|
-
# Pick the LATEST version of this contract id (matches the same
|
|
322
|
-
# rule `escrow derive-condition-hash` uses).
|
|
323
|
-
return max(matches, key=lambda r: r.get("version", 0))
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def fetch_contract_pricing(rel_id: str, contract_id: str) -> str:
|
|
327
|
-
"""`pricingModel` of the latest contract version ("" if missing)."""
|
|
328
|
-
row = fetch_contract_row(rel_id, contract_id)
|
|
329
|
-
return str(row.get("pricingModel") or "") if row else ""
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def auto_sign_contract(rel_id: str, contract_id: str):
|
|
333
|
-
"""
|
|
334
|
-
Auto-sign a contract proposal.
|
|
335
|
-
|
|
336
|
-
Pricing-model guard: this template only handles `flat` contracts
|
|
337
|
-
(full-release settlement). For `usage_based` contracts the
|
|
338
|
-
on-chain `partial_release` ix needs a different digest
|
|
339
|
-
(`ARP-SOLANA-PARTIAL-RELEASE-v1.5` + `--partial-payee-amount`)
|
|
340
|
-
that this template doesn't generate — auto-signing one of those
|
|
341
|
-
here would let the buyer release the FULL lock amount via the
|
|
342
|
-
payee signature we ship in `auto_settlement_sign`. **Refuse to
|
|
343
|
-
sign so funds can't be misrouted.**
|
|
344
|
-
|
|
345
|
-
Other policy gates (price ceiling, scope match, deadline window)
|
|
346
|
-
should go here too — this template signs anything `flat`.
|
|
347
|
-
"""
|
|
348
|
-
row = fetch_contract_row(rel_id, contract_id)
|
|
349
|
-
if row is None:
|
|
350
|
-
# Couldn't fetch the row — refuse rather than guess. The
|
|
351
|
-
# operator can investigate (e.g. server rejected the list)
|
|
352
|
-
# and sign manually.
|
|
353
|
-
print(
|
|
354
|
-
f"[worker] ✗ refusing to sign contract {contract_id[:20]} — "
|
|
355
|
-
"couldn't fetch the contract row from contracts --json. "
|
|
356
|
-
"Run `heyarp contracts <rel-id>` manually to investigate."
|
|
357
|
-
)
|
|
358
|
-
return
|
|
359
|
-
# Proactive idempotency: a contract that's already `active` was
|
|
360
|
-
# signed by a previous (re-delivered) event. Detecting it here by
|
|
361
|
-
# STATE is cleaner than reacting to a sign-time error — `contract
|
|
362
|
-
# sign` with no `--version` fails inside resolveTargetVersion (no
|
|
363
|
-
# PROPOSED row) with a generic CLI_ERROR that can't be told apart
|
|
364
|
-
# from a real failure. `replaced` (superseded by a counter) and
|
|
365
|
-
# `declined` are also terminal — nothing to sign.
|
|
366
|
-
state = row.get("state")
|
|
367
|
-
if state in ("active", "replaced", "declined"):
|
|
368
|
-
print(f"[worker] - contract already {state} (idempotent skip)")
|
|
369
|
-
return
|
|
370
|
-
pricing = str(row.get("pricingModel") or "")
|
|
371
|
-
if pricing != "flat":
|
|
372
|
-
print(
|
|
373
|
-
f"[worker] ✗ refusing to sign contract {contract_id[:20]} — "
|
|
374
|
-
f"pricingModel={pricing!r}; this template only handles `flat`. "
|
|
375
|
-
"Decline manually or upgrade to a template that signs "
|
|
376
|
-
"ARP-SOLANA-PARTIAL-RELEASE-v1.5 digests."
|
|
377
|
-
)
|
|
378
|
-
return
|
|
379
|
-
print(f"[worker] Auto-signing FLAT contract {contract_id[:20]}...")
|
|
380
|
-
# Pin the exact version we just read as PROPOSED. Without
|
|
381
|
-
# `--version`, `contract sign` auto-resolves the latest PROPOSED
|
|
382
|
-
# row — and if a concurrent signer flipped it to active in the
|
|
383
|
-
# gap, that auto-resolve fails with an ambiguous CLI_ERROR (no
|
|
384
|
-
# PROPOSED row) that's indistinguishable from a real failure.
|
|
385
|
-
# Pinning the version turns that race into a clean contract-state
|
|
386
|
-
# reject the handler below recognises.
|
|
387
|
-
cmd = [HEYARP, "contract", "sign", rel_id, contract_id, "--json"]
|
|
388
|
-
version = row.get("version")
|
|
389
|
-
if version is not None:
|
|
390
|
-
cmd += ["--version", str(version)]
|
|
391
|
-
r = shell(cmd, timeout=30)
|
|
392
|
-
if r["ok"]:
|
|
393
|
-
print("[worker] ✓ contract signed")
|
|
394
|
-
else:
|
|
395
|
-
# Rare race: a concurrent signer flipped the pinned version to
|
|
396
|
-
# active between our state read and our sign. The server
|
|
397
|
-
# rejects with a contract-state code; anything else is a
|
|
398
|
-
# genuine failure.
|
|
399
|
-
code = error_code(r)
|
|
400
|
-
if code in ("CONTRACT_INVALID_STATE", "DOM_INVALID_TRANSITION"):
|
|
401
|
-
print("[worker] - contract signed concurrently (race; idempotent skip)")
|
|
402
|
-
else:
|
|
403
|
-
print(f"[worker] ✗ contract sign failed: {code or r.get('stderr', '')[:200]}")
|
|
404
|
-
|
|
405
|
-
|
|
406
293
|
def auto_accept_delegation(rel_id: str, delegation_id: str):
|
|
407
294
|
"""
|
|
408
295
|
Auto-accept a delegation offer.
|
|
409
296
|
|
|
297
|
+
Terms are INLINE on the offer (there is no separate contract step):
|
|
298
|
+
the delegation row carries `pricingModel`, `scopeSummary`,
|
|
299
|
+
`settlementModel`, rate, amount, currency, and `deadline` — the
|
|
300
|
+
same terms the `condition_hash` binds to the escrow lock. So both
|
|
301
|
+
policy gates below (pricing model + settleable deadline) read
|
|
302
|
+
straight off the delegation row.
|
|
303
|
+
|
|
304
|
+
Pricing-model gate: this template only handles `flat` offers
|
|
305
|
+
(full-release settlement). For `usage_based` offers the on-chain
|
|
306
|
+
`partial_release` ix needs a different digest
|
|
307
|
+
(`ARP-SOLANA-PARTIAL-RELEASE-v1.5` + `--partial-payee-amount`).
|
|
308
|
+
The folded settlement step in `receipt propose` DOES generate it
|
|
309
|
+
(auto-detected from the receipt's usage.computed_amount), but this
|
|
310
|
+
template's `receipt propose` call doesn't pass `--computed-amount`,
|
|
311
|
+
so a usage_based settle would refuse downstream and strand the work
|
|
312
|
+
— refuse the offer up-front instead. To support usage_based work,
|
|
313
|
+
relax this gate AND pass `--computed-amount` on the propose call.
|
|
314
|
+
|
|
410
315
|
Settleability policy gate: this template auto-settles each receipt
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
server-side buffer). A delegation
|
|
414
|
-
auto-settled —
|
|
415
|
-
that might exceed the on-chain lock (a
|
|
416
|
-
that burns a sender_sequence). If we
|
|
417
|
-
we'd do the work, propose a receipt,
|
|
418
|
-
it — so we read the delegation row
|
|
316
|
+
via the settlement step folded into `receipt propose` (escrow),
|
|
317
|
+
which derives the payee signature's `expires_at` from the
|
|
318
|
+
delegation's `deadline` (+ the server-side buffer). A delegation
|
|
319
|
+
with NO deadline can't be auto-settled — propose refuses to
|
|
320
|
+
fabricate an expiry that might exceed the on-chain lock (a
|
|
321
|
+
post-commit server reject that burns a sender_sequence). If we
|
|
322
|
+
accepted a deadline-less offer we'd do the work, propose a receipt,
|
|
323
|
+
then fail to settle and strand it — so we read the delegation row
|
|
324
|
+
and refuse deadline-less offers.
|
|
419
325
|
|
|
420
326
|
Read-model lag: the offer SSE event can arrive BEFORE the
|
|
421
327
|
`delegations` read-model row materializes, so a missing row is
|
|
@@ -449,8 +355,21 @@ def auto_accept_delegation(rel_id: str, delegation_id: str):
|
|
|
449
355
|
f"[worker] ✗ refusing delegation {delegation_id[:20]} — "
|
|
450
356
|
"no deadline set; this template auto-settles via "
|
|
451
357
|
"deadline+buffer and can't derive a safe settlement expiry. "
|
|
452
|
-
"Accept manually + run `
|
|
453
|
-
"--expires-at <unix-secs>` to handle it."
|
|
358
|
+
"Accept manually + run `heyarp receipt propose ... "
|
|
359
|
+
"--cluster-tag <0|1> --expires-at <unix-secs>` to handle it."
|
|
360
|
+
)
|
|
361
|
+
return
|
|
362
|
+
# Pricing-model gate: terms ride inline on the offer, so the
|
|
363
|
+
# delegation row carries `pricingModel` directly (no contract
|
|
364
|
+
# lookup). Default to 'flat' when absent (no-escrow / unpriced
|
|
365
|
+
# offers). Refuse usage_based — see the docstring for why.
|
|
366
|
+
if row is not None and str(row.get("pricingModel") or "flat") != "flat":
|
|
367
|
+
print(
|
|
368
|
+
f"[worker] ✗ refusing delegation {delegation_id[:20]} — "
|
|
369
|
+
f"pricingModel={row.get('pricingModel')!r}; this template only "
|
|
370
|
+
"handles `flat`. Decline manually, or relax this gate AND pass "
|
|
371
|
+
"`--computed-amount` on the receipt propose call to support "
|
|
372
|
+
"ARP-SOLANA-PARTIAL-RELEASE-v1.5 settlement."
|
|
454
373
|
)
|
|
455
374
|
return
|
|
456
375
|
if row is None:
|
|
@@ -553,26 +472,32 @@ def handle_work_request(params: dict, rel_id: str, del_id: str,
|
|
|
553
472
|
# SETTLEMENT
|
|
554
473
|
# ═══════════════════════════════════════════════════════════════
|
|
555
474
|
#
|
|
556
|
-
#
|
|
557
|
-
# signatures
|
|
558
|
-
#
|
|
559
|
-
#
|
|
560
|
-
# A single command collapses the whole ~90-line ritual (resolve buyer
|
|
561
|
-
# key → derive condition_hash → fetch amount/decimals/deadline →
|
|
562
|
-
# compute expiry → sign release digest → deliver via
|
|
563
|
-
# settlement_signature envelope) into ONE command:
|
|
475
|
+
# To release escrow on Solana the buyer needs BOTH parties' settlement
|
|
476
|
+
# signatures. The payee (us) must generate its signature and deliver it
|
|
477
|
+
# to the buyer.
|
|
564
478
|
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
479
|
+
# This is now FOLDED into `receipt propose`: for an escrow delegation,
|
|
480
|
+
# the SAME `heyarp receipt propose ... --cluster-tag <0|1>` command that
|
|
481
|
+
# commits the receipt also signs + delivers the payee settlement
|
|
482
|
+
# signature right after — collapsing the whole ~90-line ritual (resolve
|
|
483
|
+
# buyer key → derive condition_hash → fetch amount/decimals/deadline →
|
|
484
|
+
# compute expiry → sign release digest → deliver via settlement_signature
|
|
485
|
+
# envelope) so a worker can't forget the separate step. The main loop
|
|
486
|
+
# below just reads the `settlement` block off the propose --json result.
|
|
487
|
+
# The folded step:
|
|
569
488
|
# - auto-resolves rel-id, buyer DID, condition_hash, amount,
|
|
570
|
-
# decimals, deadline, and the receipt hashes
|
|
489
|
+
# decimals, deadline, and the receipt hashes
|
|
571
490
|
# - auto-detects FULL (flat) vs PARTIAL (usage_based, from the
|
|
572
491
|
# receipt's usage.computed_amount) release
|
|
573
492
|
# - is idempotent (skips when the payee sig was already delivered)
|
|
493
|
+
# - on partial failure (no RPC / lock not visible) leaves the receipt
|
|
494
|
+
# committed and reports {delivered: false, error, recovery}; recover
|
|
495
|
+
# with `heyarp receipt send-payee-sig` (the no-RPC primitive) or
|
|
496
|
+
# re-run propose once the lock is reachable.
|
|
574
497
|
#
|
|
575
|
-
# Requires `@heyanon-arp/cli`
|
|
498
|
+
# Requires `@heyanon-arp/cli` with the folded `receipt propose` escrow
|
|
499
|
+
# settlement (the standalone `settlement auto-sign-and-deliver` command
|
|
500
|
+
# was removed).
|
|
576
501
|
|
|
577
502
|
|
|
578
503
|
# ═══════════════════════════════════════════════════════════════
|
|
@@ -682,14 +607,14 @@ def _preflight():
|
|
|
682
607
|
file=sys.stderr,
|
|
683
608
|
)
|
|
684
609
|
sys.exit(2)
|
|
685
|
-
# Settlement is now signed by `
|
|
610
|
+
# Settlement is now signed by the `receipt propose` escrow step
|
|
686
611
|
# using the AGENT's stored settlement key (resolved from local
|
|
687
612
|
# heyarp state), NOT the MY_SETTLEMENT_PUBKEY constant. So this
|
|
688
613
|
# value is only meaningful as an assertion: verify it matches the
|
|
689
614
|
# key the worker will actually be paid on, otherwise funds would
|
|
690
615
|
# arrive at the agent's address and you'd only find out after a job
|
|
691
616
|
# settles. Uses the same sole-agent resolution as the settlement
|
|
692
|
-
#
|
|
617
|
+
# step (run a single worker per DID — see README caveats).
|
|
693
618
|
who = shell_json([HEYARP, "whoami", "--local", "--json"], timeout=15)
|
|
694
619
|
if who is None:
|
|
695
620
|
print(
|
|
@@ -705,7 +630,7 @@ def _preflight():
|
|
|
705
630
|
print(
|
|
706
631
|
f"[worker] FATAL: MY_SETTLEMENT_PUBKEY ({MY_SETTLEMENT_PUBKEY})\n"
|
|
707
632
|
f" does not match your heyarp agent's settlement key ({agent_key}).\n"
|
|
708
|
-
" `
|
|
633
|
+
" `receipt propose` (escrow) signs with the AGENT's key, so\n"
|
|
709
634
|
" funds would route to the agent's address, not the one set here.\n"
|
|
710
635
|
" Fix ARP_SETTLEMENT_PUBKEY to match `heyarp whoami --local`, or\n"
|
|
711
636
|
" re-register the agent with the intended settlement key.",
|
|
@@ -734,8 +659,8 @@ def main():
|
|
|
734
659
|
print(f"[worker] Cluster tag: {CLUSTER_TAG} ({'mainnet-beta' if CLUSTER_TAG == '1' else 'devnet'})")
|
|
735
660
|
print(f"[worker] Mint: {MINT_PUBKEY} ({'native SOL' if MINT_PUBKEY == NATIVE_SOL_MINT else 'SPL token'})")
|
|
736
661
|
print(f"[worker] Working dir: {WORK_DIR}")
|
|
737
|
-
print("[worker] Auto-accept: handshake ✓ |
|
|
738
|
-
print("[worker] Auto-settle: receipt → sign →
|
|
662
|
+
print("[worker] Auto-accept: handshake ✓ | delegation ✓")
|
|
663
|
+
print("[worker] Auto-settle: receipt → sign → deliver → buyer\n")
|
|
739
664
|
|
|
740
665
|
proc = _spawn_inbox()
|
|
741
666
|
|
|
@@ -776,14 +701,6 @@ def main():
|
|
|
776
701
|
print(f"\n[worker] >>> Handshake from {sender_did[:30]}...")
|
|
777
702
|
auto_accept_handshake(sender_did)
|
|
778
703
|
|
|
779
|
-
elif body_type == "contract":
|
|
780
|
-
content = body.get("content", {})
|
|
781
|
-
if content.get("action") == "proposal":
|
|
782
|
-
rel_id = envelope.get("relationshipId", "")
|
|
783
|
-
contract_id = content.get("contract_id", "")
|
|
784
|
-
print(f"\n[worker] >>> Contract proposal {contract_id[:20]}...")
|
|
785
|
-
auto_sign_contract(rel_id, contract_id)
|
|
786
|
-
|
|
787
704
|
elif body_type == "delegation":
|
|
788
705
|
content = body.get("content", {})
|
|
789
706
|
if content.get("action") == "offer":
|
|
@@ -814,53 +731,76 @@ def main():
|
|
|
814
731
|
print("[worker] - receipt already exists for delegation, skipping")
|
|
815
732
|
continue
|
|
816
733
|
|
|
817
|
-
# 4. Auto-propose receipt
|
|
818
|
-
|
|
819
|
-
receipt
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
# 5. Sign + deliver the payee settlement signature in
|
|
831
|
-
# ONE command. It resolves the buyer key,
|
|
832
|
-
# condition hash, amount/decimals/deadline, and the
|
|
833
|
-
# receipt hashes itself, signs the release digest,
|
|
834
|
-
# and delivers it to the buyer via a
|
|
835
|
-
# settlement_signature envelope. Idempotent:
|
|
836
|
-
# re-running after delivery is a no-op. Auto-detects
|
|
837
|
-
# full (flat) vs partial (usage_based) release.
|
|
734
|
+
# 4. Auto-propose receipt AND deliver the payee
|
|
735
|
+
# settlement signature in ONE command (--json output).
|
|
736
|
+
# For an ESCROW delegation, `receipt propose` signs +
|
|
737
|
+
# delivers the payee settlement signature itself right
|
|
738
|
+
# after committing the receipt — the worker can no
|
|
739
|
+
# longer forget a separate step (an undelivered sig
|
|
740
|
+
# would strand the buyer's cosign on
|
|
741
|
+
# ESC_SETTLEMENT_SIGS_MISSING). It resolves the buyer
|
|
742
|
+
# key, condition hash, amount/decimals/deadline, and
|
|
743
|
+
# the receipt hashes itself, signs the release digest,
|
|
744
|
+
# and delivers via a settlement_signature envelope.
|
|
745
|
+
# Idempotent on re-run; auto-detects full (flat) vs
|
|
746
|
+
# partial (usage_based) release.
|
|
838
747
|
#
|
|
839
|
-
#
|
|
840
|
-
#
|
|
841
|
-
#
|
|
842
|
-
# one proposed receipt (multiple work requests).
|
|
748
|
+
# --cluster-tag is REQUIRED for an escrow delegation
|
|
749
|
+
# (binds the cluster into the signed release digest;
|
|
750
|
+
# a wrong/defaulted cluster fails at the buyer cosign).
|
|
843
751
|
# --fee-bps-at-lock 0: this template targets no-fee
|
|
844
752
|
# deployments (see README). The release digest binds
|
|
845
753
|
# the lock's fee snapshot; passing 0 makes that
|
|
846
754
|
# assumption explicit. On a fee-CHARGING deployment
|
|
847
755
|
# you must pass the lock's actual fee_bps_at_lock +
|
|
848
756
|
# --fee-recipient-at-lock instead.
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
757
|
+
print("[worker] Auto-proposing receipt (+ delivering settlement sig for escrow)...")
|
|
758
|
+
# Use shell() (not shell_json) so we still capture the
|
|
759
|
+
# JSON on a PARTIAL-FAILURE exit: an escrow propose that
|
|
760
|
+
# commits the receipt but fails to deliver the settlement
|
|
761
|
+
# sig (no RPC / lock not yet visible) exits non-zero while
|
|
762
|
+
# still emitting the `settlement` block on stdout. Gating
|
|
763
|
+
# on `is None` (shell_json) would skip the recovery handler
|
|
764
|
+
# below and loop into RECEIPT_ALREADY_EXISTS.
|
|
765
|
+
proposal = shell([
|
|
766
|
+
HEYARP, "receipt", "propose", sender,
|
|
767
|
+
del_id, "--auto-hashes", "--rel-id", rel_id,
|
|
768
|
+
"--request-id", req_id, "--verdict", "accepted",
|
|
852
769
|
"--cluster-tag", CLUSTER_TAG, "--mint-pubkey", MINT_PUBKEY,
|
|
853
770
|
"--fee-bps-at-lock", "0",
|
|
854
771
|
"--json",
|
|
855
|
-
]
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
772
|
+
], timeout=30)
|
|
773
|
+
receipt = None
|
|
774
|
+
if proposal["stdout"]:
|
|
775
|
+
try:
|
|
776
|
+
receipt = json.loads(proposal["stdout"])
|
|
777
|
+
except Exception:
|
|
778
|
+
receipt = None
|
|
779
|
+
if receipt is None:
|
|
780
|
+
print(f"[worker] - receipt propose failed: {proposal.get('stderr') or proposal.get('error') or 'no parseable output'}")
|
|
781
|
+
continue
|
|
782
|
+
print("[worker] ✓ receipt proposed" + ("" if proposal["ok"] else " (settlement step reported a problem — see below)"))
|
|
783
|
+
|
|
784
|
+
# 5. Report the folded settlement outcome. For an escrow
|
|
785
|
+
# delegation `receipt propose` returns a `settlement`
|
|
786
|
+
# block: {delivered, purpose, ...} on success, or
|
|
787
|
+
# {delivered: false, error, recovery} on partial
|
|
788
|
+
# failure (no RPC / lock not yet visible). The receipt
|
|
789
|
+
# is committed either way; on partial failure re-run
|
|
790
|
+
# propose once the lock is reachable, or follow the
|
|
791
|
+
# `recovery` hint (`heyarp receipt send-payee-sig`).
|
|
792
|
+
settle = receipt.get("settlement")
|
|
793
|
+
if settle is None:
|
|
794
|
+
# Prepaid (non-escrow) delegation — no settlement step.
|
|
795
|
+
print("[worker] ✓ no settlement step (prepaid delegation)")
|
|
796
|
+
elif settle.get("delivered"):
|
|
860
797
|
tag = " (idempotent)" if settle.get("idempotent") else ""
|
|
861
798
|
print(f"[worker] ✓ settlement signed + delivered: {settle.get('purpose')}{tag}")
|
|
862
799
|
else:
|
|
863
|
-
print(
|
|
800
|
+
print(
|
|
801
|
+
"[worker] ✗ receipt proposed but settlement sig NOT delivered: "
|
|
802
|
+
f"{settle.get('error', 'unknown')}. Recover: {settle.get('recovery', '')}"
|
|
803
|
+
)
|
|
864
804
|
|
|
865
805
|
except Exception as e:
|
|
866
806
|
print(f"[worker] ✗ Error: {e}")
|
|
@@ -877,9 +817,9 @@ def main():
|
|
|
877
817
|
pass
|
|
878
818
|
|
|
879
819
|
else:
|
|
880
|
-
# Other event types (receipt cosign,
|
|
881
|
-
#
|
|
882
|
-
#
|
|
820
|
+
# Other event types (receipt cosign, dispute, etc.) —
|
|
821
|
+
# logged but not auto-handled. Add handlers here if you
|
|
822
|
+
# need them.
|
|
883
823
|
if body_type:
|
|
884
824
|
print(f"[worker] event: {body_type}", end="\r")
|
|
885
825
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyanon-arp/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Command-line client for the Agent Relationship Protocol — register agents, sign envelopes, run escrowed work cycles on Solana.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"examples",
|
|
26
|
+
"scripts/postinstall.mjs",
|
|
26
27
|
"LICENSE",
|
|
27
28
|
"README.md"
|
|
28
29
|
],
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"commander": "^12.1.0",
|
|
37
38
|
"prompts": "^2.4.2",
|
|
38
39
|
"simple-update-notifier": "^2.0.0",
|
|
39
|
-
"@heyanon-arp/sdk": "0.0.
|
|
40
|
+
"@heyanon-arp/sdk": "0.0.6",
|
|
41
|
+
"@heyanon-arp/shield": "0.0.1"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@types/jest": "^29.5.2",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"build": "tsup",
|
|
53
55
|
"start": "node dist/cli.js",
|
|
54
56
|
"test": "jest --runInBand --detectOpenHandles --forceExit --passWithNoTests",
|
|
55
|
-
"lint": "biome check . --write"
|
|
57
|
+
"lint": "biome check . --write",
|
|
58
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
56
59
|
}
|
|
57
60
|
}
|