Skip to content

feat: add TaskMarket delegation plugin - #470

Open
a1271981054 wants to merge 1 commit into
OpenHands:mainfrom
a1271981054:codex/taskmarket-openhands-plugin
Open

a1271981054 wants to merge 1 commit into
OpenHands:mainfrom
a1271981054:codex/taskmarket-openhands-plugin

Conversation

@a1271981054

@a1271981054 a1271981054 commented Aug 13, 2026 •

Copy link
Copy Markdown

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

  • Add a taskmarket OpenHands plugin with a Python adapter for public task
    discovery, task status, and submission review.
  • Add a deterministic preview showing the exact description, reward, deadline,
    Base/USDC network, and a fee-buffered maximum-spend estimate.
  • Add a guarded create path that checks the first-party CLI's reported network
    and invokes taskmarket task create only after --confirm and a spend
    ceiling are supplied.
  • Never read the CLI keystore, automatically accept submissions, or retry an
    ambiguous payment response.

How to Test

uv run --group test pytest -q tests/
# 550 passed, 6 skipped

python plugins/taskmarket/scripts/taskmarket.py list \
  --status open --sort reward_desc --limit 5

python plugins/taskmarket/scripts/taskmarket.py preview \
  --description "Produce a tested report with reproducible evidence" \
  --reward 0.50 --duration 24 \
  --tags research,verification --max-spend-usdc 0.55

The live read smoke test returned the public TaskMarket task list. The
no-confirm create path returned confirmationRequired: true and did not
invoke the CLI.

Security and spending notes

  • The write path is restricted to the TaskMarket CLI's Base chain (8453) and
    does not implement a second wallet or payment mechanism.
  • A positive USDC reward, one to ten tags, and a maximum-spend ceiling are
    validated before any CLI call.
  • The adapter runs the read-only taskmarket deposit check first and rejects
    non-Base wallets.
  • A failed create returns retry: false; callers are instructed to inspect
    the live task and idempotency information before deciding what happened.
  • The plugin exposes status and submissions for human review only. It does
    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.

@github-actions github-actions Bot added the type: feat A new feature label Aug 13, 2026
@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

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 all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from origin/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-title jobs (passing). Three pull_request-triggered runs ("Tests", "Check Extensions", "Deprecation deadlines") show failure for 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.

  1. The approved spend ceiling never reaches the payment. --max-spend-usdc is only compared against the adapter's own local estimate (max_spend < estimated), then the CLI is invoked with --reward and no ceiling. The first-party CLI's task create has no spend-cap option, and its x402Post signs a transfer for the server-supplied accept.amount without comparing it to any local budget. The amount actually authorized is therefore bounded only by the --reward the adapter passes plus whatever the server quotes, not by the user-approved ceiling. SKILL.md states the opposite, which is not a behavior the CLI implements.

  2. An ambiguous paid create is reported as unsubmitted. If the CLI call for create times out (120s) or fails at the OS level, run_cli raises AdapterError; main catches all AdapterErrors and emits {"success": false, "notSubmitted": true, "error": ...} with exit 2. A timeout can occur after the CLI has already broadcast the payment/creation, so notSubmitted: true can be factually wrong precisely in the ambiguous case this plugin claims to handle conservatively. The separate nonzero-return-code path below correctly emits retry: false / paymentState: "unknown_or_not_settled", so the two paths are inconsistent.

  3. Out-of-range numeric magnitudes crash with a raw traceback instead of the JSON contract. parse_usdc / format_usdc call quantize unguarded, and deadline_from_duration converts through float. A large reward (e.g. --reward 1e30) raises decimal.InvalidOperation, and a large duration raises the same plus OverflowError. Because these are not AdapterError, main does 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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@enyst enyst left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The command pre-approves the whole wallet CLI. delegate.md:2 sets allowed-tools: Bash(python:*), Bash(taskmarket:*), Bash(npm:*). In Claude Code (through the .claude-plugin link), that lets the agent run any taskmarket subcommand without a prompt, including task create, task accept and withdraw, plus any npm command. That bypasses every guard in the adapter. Allow only the adapter.
  2. The agent supplies its own confirmation. The only gate between preview and payment is the --confirm flag, which the agent adds itself (SKILL.md:47-48, taskmarket.py:360). A flag is not authorization. Use host-level confirmation, or keep create as a step the user runs.
  3. Task text becomes public. No visibility flag is passed (taskmarket.py:381-394), so tasks are created public, and the CLI's own help says even unlisted tasks stay permanently visible on-chain. SKILL.md does say to remove secrets, but it should also say the description becomes permanently public.
  4. auction mode can't work. ALLOWED_MODES includes auction (taskmarket.py:35), but the adapter never passes the --max-price and --auction-type options the CLI requires for it.
  5. Paths and install. ADAPTER="plugins/taskmarket/scripts/taskmarket.py" (SKILL.md:61) only resolves from an extensions checkout, while other plugins use skill-relative scripts/... paths. npm install -g @lucid-agents/taskmarket is 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.
  6. 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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants