Repository navigation
feat: add TaskMarket delegation plugin - #470
a1271981054 wants to merge 1 commit into
Conversation
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Reviewed head 195b05ff92bf7dfdea9efec4d67abb7548b1313e (matches the requested SHA; PR open, base main, labels type: feat, no prior reviews or review threads). The change is in scope for this registry: it adds plugins/taskmarket/ following the existing plugin layout (SKILL.md, .plugin/plugin.json, vendor symlinks, commands/, scripts/) and registers it in marketplaces/openhands-extensions.json.
Checks I ran on this exact head:
uv run --group test pytest -q tests/-> 550 passed, 6 skipped.python scripts/sync_extensions.py --check-> clean except a pre-existing coverage warning for./plugins/issue-duplicate-checker, which is also absent fromorigin/main.uv run python scripts/check_deprecations.py-> clean.- Live read smoke (
list,status <id>,submissions <id>) returned real public data, so the read path works. - The only base-repo checks attached to this head are the two
pr-titlejobs (passing). Threepull_request-triggered runs ("Tests", "Check Extensions", "Deprecation deadlines") showfailurefor this SHA, but they executed in the fork (head_repository.fork = true) and their logs are no longer retrievable; the equivalent local checks above pass, so I cannot attribute those failures to this change.
Blocking state: mergeable_state is dirty and the merge is not rebaseable. main has moved on since this branch's base (bc6017d), and the merge-tree conflict is in the auto-generated catalog section of README.md (plus the marketplaces/openhands-extensions.json / README counts that the sync script regenerates). This needs a rebase and a re-run of scripts/sync_extensions.py before it can land.
Findings below. The first is the important one: the PR's headline spending guarantee is not enforced on the payment path.
-
The approved spend ceiling never reaches the payment.
--max-spend-usdcis only compared against the adapter's own local estimate (max_spend < estimated), then the CLI is invoked with--rewardand no ceiling. The first-party CLI'stask createhas no spend-cap option, and itsx402Postsigns a transfer for the server-suppliedaccept.amountwithout comparing it to any local budget. The amount actually authorized is therefore bounded only by the--rewardthe adapter passes plus whatever the server quotes, not by the user-approved ceiling.SKILL.mdstates the opposite, which is not a behavior the CLI implements. -
An ambiguous paid create is reported as unsubmitted. If the CLI call for
createtimes out (120s) or fails at the OS level,run_cliraisesAdapterError;maincatches allAdapterErrors and emits{"success": false, "notSubmitted": true, "error": ...}with exit 2. A timeout can occur after the CLI has already broadcast the payment/creation, sonotSubmitted: truecan be factually wrong precisely in the ambiguous case this plugin claims to handle conservatively. The separate nonzero-return-code path below correctly emitsretry: false/paymentState: "unknown_or_not_settled", so the two paths are inconsistent. -
Out-of-range numeric magnitudes crash with a raw traceback instead of the JSON contract.
parse_usdc/format_usdccallquantizeunguarded, anddeadline_from_durationconverts throughfloat. A large reward (e.g.--reward 1e30) raisesdecimal.InvalidOperation, and a large duration raises the same plusOverflowError. Because these are notAdapterError,maindoes not catch them: the caller (an agent) gets a Python traceback and no JSON. Since these arguments can be built from prompt content, the adapter should reject the magnitude explicitly.
Non-blocking: plugins/taskmarket/.plugin/plugin.json credits "author": {"name": "Community contributor"} with no identifying handle or email, unlike the other manifests in this repo. Not a merge blocker.
🔄 CHANGES REQUESTED
|
|
||
| max_spend = parse_usdc(args.max_spend_usdc, field="max-spend-usdc") | ||
| estimated = parse_usdc(preview["estimatedMaxSpendUsdc"], field="estimated-max-spend-usdc") | ||
| if max_spend < estimated: |
There was a problem hiding this comment.
The user-approved ceiling is only checked against the adapter's own local estimate and is never passed to the CLI. task create in @lucid-agents/taskmarket has no spend-cap option, and its x402 payment path signs a transfer for the server-supplied accept.amount without comparing it to any local budget (see createTransferAuthorization / x402Post in the CLI). So after --confirm, the amount that can actually be authorized is bounded by the --reward passed below plus whatever the server quotes, not by --max-spend-usdc. This defeats the PR's stated guarantee that a higher live quote fails before settlement; that handshake does not exist in the CLI. Consider surfacing this limitation (and removing the corresponding claim in SKILL.md) or passing a real cap the CLI enforces.
| try: | ||
| return run(args) | ||
| except AdapterError as exc: | ||
| emit({"success": False, "notSubmitted": True, "error": str(exc)}) |
There was a problem hiding this comment.
This catch-all turns a create failure into notSubmitted: true. A CLI invocation that times out or fails at the OS level raises AdapterError from run_cli and lands here, even though the CLI may have already broadcast the payment and created the task. notSubmitted: true is then wrong in exactly the ambiguous case the plugin says it handles conservatively. The nonzero-return-code path emits retry: false / paymentState: "unknown_or_not_settled" instead, so the two paths disagree. On the post-confirmation CLI failure path, prefer the same ambiguous/unknown wording rather than asserting nothing was submitted.
| raise AdapterError(f"{field} must be {minimum}") | ||
| if amount.as_tuple().exponent < -6: | ||
| raise AdapterError(f"{field} supports at most six decimal places") | ||
| return amount.quantize(USDC_QUANTUM) |
There was a problem hiding this comment.
quantize is unguarded, so out-of-range magnitudes raise decimal.InvalidOperation (e.g. --reward 1e30). deadline_from_duration has the same problem via float(duration * 3600) raising OverflowError. Neither is an AdapterError, so main's handler does not catch them and the process exits with a Python traceback and no JSON, unlike every other error path. Since these values can be assembled from prompt content, they should be range-checked and rejected with an AdapterError.
| - The supplied maximum spend must cover the reward plus a configurable | ||
| 7.5%-style platform-fee estimate and a 0.001 USDC relay buffer. If it does | ||
| not, the CLI is never invoked. | ||
| - If the live platform quote is higher than the ceiling, the first-party CLI |
There was a problem hiding this comment.
This claim is not backed by the first-party CLI: task create has no ceiling flag, and its payment path signs the server-quoted amount without a local comparison, so a higher live quote does not "fail before settlement" at the ceiling. Please correct this line to match what the adapter and CLI actually enforce.
There was a problem hiding this comment.
I'm an AI agent (Claude Code, based on Opus 5.5) helping Engel Nyst (@enyst) with project work.
Requesting changes. all-hands-bot's 2026-09-24 findings (spend ceiling not enforced, timeout reported as notSubmitted, large values crash, conflicts) are still open on 195b05f; no commits have landed since.
Additional points:
- The command pre-approves the whole wallet CLI.
delegate.md:2setsallowed-tools: Bash(python:*), Bash(taskmarket:*), Bash(npm:*). In Claude Code (through the.claude-pluginlink), that lets the agent run anytaskmarketsubcommand without a prompt, includingtask create,task acceptandwithdraw, plus anynpmcommand. That bypasses every guard in the adapter. Allow only the adapter. - The agent supplies its own confirmation. The only gate between preview and payment is the
--confirmflag, which the agent adds itself (SKILL.md:47-48,taskmarket.py:360). A flag is not authorization. Use host-level confirmation, or keepcreateas a step the user runs. - Task text becomes public. No visibility flag is passed (
taskmarket.py:381-394), so tasks are createdpublic, and the CLI's own help says evenunlistedtasks stay permanently visible on-chain. SKILL.md does say to remove secrets, but it should also say the description becomes permanently public. auctionmode can't work.ALLOWED_MODESincludesauction(taskmarket.py:35), but the adapter never passes the--max-priceand--auction-typeoptions the CLI requires for it.- Paths and install.
ADAPTER="plugins/taskmarket/scripts/taskmarket.py"(SKILL.md:61) only resolves from an extensions checkout, while other plugins use skill-relativescripts/...paths.npm install -g @lucid-agents/taskmarketis unpinned: its latest version, 1.13.2, was published 2026-10-03, inside the seven-day recency window, and the package links no source repository. - Process. There's no linked issue.
Compared with #471 (same service, different author): #471 exposes fewer tools but its MCP framing is broken. This PR works as a CLI adapter but pre-approves the whole wallet CLI. Neither enforces consent or spend where the payment happens. The same integration has been proposed to many agent frameworks, and no OpenHands user request is linked. I'd recommend adding neither unless maintainers accept #473.
Why
OpenHands agents can identify work that is better delegated to an external
worker, but the extensions registry does not currently provide a TaskMarket
workflow. This plugin adds a bounded integration while keeping payment and
result acceptance under explicit user control.
Summary
taskmarketOpenHands plugin with a Python adapter for public taskdiscovery, task status, and submission review.
Base/USDC network, and a fee-buffered maximum-spend estimate.
and invokes
taskmarket task createonly after--confirmand a spendceiling are supplied.
ambiguous payment response.
How to Test
The live read smoke test returned the public TaskMarket task list. The
no-confirm create path returned
confirmationRequired: trueand did notinvoke the CLI.
Security and spending notes
does not implement a second wallet or payment mechanism.
validated before any CLI call.
taskmarket depositcheck first and rejectsnon-Base wallets.
retry: false; callers are instructed to inspectthe live task and idempotency information before deciding what happened.
not select winners, accept work, or evaluate submissions automatically.
HUMAN: Test record: the full suite, catalog check, and live read-only smoke tests were run locally; the no-confirm create path stayed unsubmitted.
HUMAN: I personally ran the focused tests, catalog validation, and the read-only live smoke; no payment or submission was performed by the plugin.