@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.
@@ -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 → contract → delegation → work → receipt → settlement, all auto-mediated; you fill in `handle_work_request()`) | Reference; FLAT pricing + full-release settlement only. MIT. |
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 --endpoint-url https://my-agent.example/inbox
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: `settlement auto-sign-and-deliver` signs with the
54
- agent's stored key, so a mismatch would route funds to the agent's
55
- address rather than the one you set here. The preflight resolves the
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.** The
92
- template's `auto_sign_contract` refuses to sign non-flat contracts
93
- (a full-release signature on a `usage_based` lock would let the
94
- buyer drain it). The settlement step itself
95
- `heyarp settlement auto-sign-and-deliver` DOES handle
96
- both: it auto-detects full (flat) vs partial (`usage_based`, from
97
- the receipt's `usage.computed_amount`) release and signs the right
98
- digest (`ARP-SOLANA-RELEASE-v1.5` / `ARP-SOLANA-PARTIAL-RELEASE-v1.5`).
99
- To support `usage_based` work, TWO changes are needed — not just
100
- one: (1) relax the FLAT-only gate in `auto_sign_contract`, AND
101
- (2) compute the per-task amount and pass `--computed-amount` on the
102
- `receipt propose` call (the settlement command binds the partial
103
- release to the receipt's `usage.computed_amount` and refuses to
104
- settle a usage_based receipt that lacks it).
105
- - **Settlement is one command now.** The ~90-line payee settlement
106
- ritual (resolve buyer key → derive condition_hash → fetch
107
- amount/decimals/deadline compute expiry sign deliver)
108
- collapses to `heyarp settlement auto-sign-and-deliver
109
- --delegation-id <id> --cluster-tag <0|1>`. It auto-resolves the rest
110
- from the delegation id, signs the release digest (reusing `wallet
111
- sign-settlement-release`), and delivers via a `settlement_signature`
112
- envelope the buyer consumes with `receipt cosign
113
- --auto-resolve-payee-sig`. Idempotent safe to re-run.
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
- `auto_sign_contract()` before signing.
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 → contract → delegation → work → receipt
8
- settlement) is auto-mediated.
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
- contract proposal → auto-sign
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
- settlement auto-sign-and-deliver (one command)
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.** `auto_sign_contract`
33
- below refuses to sign non-flat contracts (a full-release signature on
34
- a usage_based lock would let the buyer drain it). The underlying
35
- `heyarp settlement auto-sign-and-deliver` command DOES handle partial
36
- release (it auto-detects from the receipt's usage.computed_amount), so
37
- to support usage_based work, relax the pricing gate in
38
- `auto_sign_contract` the settlement step already does the right thing.
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 contract's `create_lock` was submitted on, otherwise
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 → contract → delegation → work →
233
- # receipt. These handlers auto-accept every non-terminal phase so your
234
- # worker runs without human intervention.
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 contract
244
- proposals from the buyer.
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 contract proposal.",
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
- with `settlement auto-sign-and-deliver`, which derives the payee
412
- signature's `expires_at` from the delegation's `deadline` (+ the
413
- server-side buffer). A delegation with NO deadline can't be
414
- auto-settled — the settle command refuses to fabricate an expiry
415
- that might exceed the on-chain lock (a post-commit server reject
416
- that burns a sender_sequence). If we accepted a deadline-less offer
417
- we'd do the work, propose a receipt, then fail to settle and strand
418
- it — so we read the delegation row and refuse deadline-less offers.
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 `settlement auto-sign-and-deliver "
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
- # After receipt propose, the buyer needs BOTH parties' settlement
557
- # signatures to release escrow on Solana. The payee (us) must
558
- # generate its signature and deliver it to the buyer.
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
- # heyarp settlement auto-sign-and-deliver --delegation-id <id>
566
- #
567
- # The main loop below calls it directly after `receipt propose` —
568
- # there's nothing left to hand-orchestrate here. The command:
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 from --delegation-id
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` 0.0.5.
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 `settlement auto-sign-and-deliver`
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
- # command (run a single worker per DID — see README caveats).
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
- " `settlement auto-sign-and-deliver` signs with the AGENT's key, so\n"
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 ✓ | contract ✓ | delegation ✓")
738
- print("[worker] Auto-settle: receipt → sign → memory → buyer\n")
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 (--json output).
818
- print("[worker] Auto-proposing receipt...")
819
- receipt = shell_json([
820
- HEYARP, "receipt", "propose", sender,
821
- del_id, "--auto-hashes", "--rel-id", rel_id,
822
- "--request-id", req_id, "--verdict", "accepted",
823
- "--json",
824
- ], timeout=30)
825
- if receipt is None:
826
- print("[worker] - receipt propose failed")
827
- continue
828
- print("[worker] receipt proposed")
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
- # Pass --receipt-event-hash from the propose result
840
- # so the command targets the EXACT receipt we just
841
- # created required when a delegation has more than
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
- settle_cmd = [
850
- HEYARP, "settlement", "auto-sign-and-deliver",
851
- "--delegation-id", del_id, "--rel-id", rel_id,
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
- if receipt.get("receiptEventHash"):
857
- settle_cmd += ["--receipt-event-hash", receipt["receiptEventHash"]]
858
- settle = shell_json(settle_cmd, timeout=30)
859
- if settle is not None:
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("[worker] - settlement auto-sign-and-deliver failed (see stderr)")
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, memory_delta,
881
- # dispute, etc.) — logged but not auto-handled. Add
882
- # handlers here if you need them.
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.5",
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.4"
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
  }