Skip to content

fix(rip302): disputed jobs are a dead end — the worker can never re-deliver and the escrow only goes back to the poster - #8086

Open
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/rip302-disputed-redelivery
Open

fix(rip302): disputed jobs are a dead end — the worker can never re-deliver and the escrow only goes back to the poster#8086
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/rip302-disputed-redelivery

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

The bug

POST /agent/jobs/<id>/dispute answers the client with:

Job disputed. Escrow held pending resolution. Worker can re-deliver or admin can refund.

Neither half was reachable:

  • /deliver accepted only claimed jobs (if j["status"] != STATUS_CLAIMED: 409), and no route moves a job back out of disputed — so the worker could never act on the rejection reason.
  • There is no admin route at all. The registered set is /agent/jobs, /agent/jobs/<id>, /claim, /deliver, /accept, /dispute, /cancel, /agent/reputation/<w>, /agent/stats.
  • A disputed job is also outside the expiry sweep (_expire_refundable_job returns early unless the status is open/claimed), so the escrow never released on its own either.

The only reachable exit was the poster calling /cancel, which refunds 100% of the escrow to the poster — after the deliverable is already stored on the job and readable by anyone via GET /agent/jobs/<id>. So on a money path, the documented recourse for the worker did not exist.

Reproduced against main (Flask test client, real endpoints, the same harness style as tests/test_agent_dispute_accept_race.py):

posted: 201 escrow held: 1050000
claim : 200
deliver: 200
dispute: 200 -> Job disputed. Escrow held pending resolution. Worker can re-deliver or admin can refund.
deliverable still readable by anyone: 'https://example.com/out.csv' 'here is the finished CSV'
worker re-deliver: 409 {'error': "Job must be in 'claimed' status (current: disputed)"}
status after TTL passed + expiry sweep: disputed | escrow still held: 1050000
poster cancel: 200 1.05 | worker balance: 0 | poster balance: 5000000   <- fully restored

The fix

Make the promise the API already prints actually true, without changing any other transition:

  • /deliver accepts disputed from the assigned worker as a re-delivery; the atomic WHERE ... AND status = ? guard now expects the status it read, so the concurrency protection from Security: Hardened P2P Sync Module (Replay Protection + Deterministic JSON) #2867 is preserved.
  • Re-delivery clears the now-stale rejection_reason (the reason stays in agent_job_log) and is logged as redelivered, so the audit trail distinguishes the retry from the first delivery.
  • The TTL gate applies to a first delivery only. A disputed job is deliberately outside the expiry sweep, so keeping the gate there would fail re-delivery with a misleading STATE_RACE once the original deadline passed.
  • The /dispute message now names only routes that exist: /deliver for the worker, /cancel for the poster. No admin endpoint is invented.

Deliberately not changed: the poster can still cancel a disputed job and be refunded. Whether a poster should be able to unilaterally reclaim escrow after receiving a deliverable is a protocol decision, not something to slip into a bugfix — this PR only removes the dead end that left the worker with no move at all.

Tests

tests/test_agent_dispute_redelivery.py — 8 cases:

  • assigned worker re-delivers a disputed job, poster accepts, worker is actually paid (worker == 1_000_000, agent_escrow == 0);
  • re-delivery clears the stale rejection_reason;
  • re-delivery is logged distinctly (redelivered, exactly one delivered);
  • a different wallet gets 403, job stays disputed;
  • re-delivery still works after the original TTL elapsed;
  • regression guardopen and completed jobs are still undeliverable (409);
  • regression guard — a claimed job past TTL still expires and refunds the poster on /deliver (410, escrow 0, poster whole);
  • the dispute message names no admin route and only registered rules.

Mutation check (revert rip302_agent_economy.py only): 6 of 8 fail on main, all 8 pass with the fix; the 2 that pass either way are the regression guards, which is the point of them.

Verification:

  • python3 -m pytest tests/test_agent_dispute_redelivery.py → 8 passed
  • full agent-economy suite (test_agent_reputation, test_agent_economy_bounties, test_agent_economy_cli, test_agent_economy_dashboard_security, test_agent_economy_v2_dashboard_security, test_agent_economy_error_redaction, test_agent_jobs_query_validation, test_agent_economy_sdk, test_rustchain_agent_cli, test_agent_dispute_accept_race, plus the new file) → 84 passed
  • git diff --check → clean
  • python3 tools/bcos_spdx_check.py --base-ref upstream/main → BCOS SPDX check: OK

Claiming under rustchain-bounties#71 · RTC wallet RTCd1554f0f35576faf01d386a6be1c947f560dd0b7

/dispute answers the client with "Worker can re-deliver or admin can
refund", but /deliver accepted only 'claimed' jobs and no route ever
moved a job back out of 'disputed'. A disputed job is also outside the
TTL expiry sweep, so its escrow never released on its own. The only
reachable exit was the poster cancelling and taking the full escrow
back - after the deliverable was already readable on GET /agent/jobs/<id>.

- /deliver now accepts 'disputed' from the assigned worker (re-delivery),
  clears the stale rejection_reason and logs the action as 'redelivered'.
- The TTL gate applies to a first delivery only: a disputed job is
  deliberately outside the expiry sweep, so gating it there would fail
  re-delivery with a misleading STATE_RACE once the original TTL passed.
- The /dispute response now names only routes that actually exist
  (/deliver for the worker, /cancel for the poster); there is no admin
  refund endpoint.

Tests: tests/test_agent_dispute_redelivery.py - 8 cases, 6 fail on main
and pass with the fix, 2 are regression guards (open/completed jobs are
still undeliverable; a claimed job past TTL still expires and refunds).
Agent-economy suite 84 passed, BCOS SPDX check OK.
@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) tests Test suite changes size/L PR: 201-500 lines labels Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) size/L PR: 201-500 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant