From df19ed54aa3fd8c84c316f79202041989b3217e4 Mon Sep 17 00:00:00 2001 From: "Nadine Roth (COO)" Date: Thu, 20 Aug 2026 16:41:54 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(mcp=5Fmodules):=20add=20taskmarket=20d?= =?UTF-8?q?elegation=20module\n\nDelegate=20real=20work=20to=20Taskmarket?= =?UTF-8?q?=20from=20the=20Profullstack=20MCP=20server:=20browse\nopen=20t?= =?UTF-8?q?asks,=20create=20a=20funded=20task=20after=20explicit=20user=20?= =?UTF-8?q?authorization,=20track\nlive=20status,=20and=20retrieve=20submi?= =?UTF-8?q?ssions=20for=20human=20review=20=E2=80=94=20all=20on=20Base\nma?= =?UTF-8?q?innet=20via=20the=20official=20taskmarket=20CLI=20(first-party?= =?UTF-8?q?=20tooling,=20no=20secrets\nhandled=20in=20this=20module).\n\nS?= =?UTF-8?q?afety=20gates=20required=20by=20the=20integration=20spec:\n-=20?= =?UTF-8?q?createTask=20/=20acceptSubmission=20require=20confirm:true=20(n?= =?UTF-8?q?o=20silent=20spending)\n-=20Base=20mainnet=20only;=20reward=20m?= =?UTF-8?q?ust=20not=20exceed=20caller-supplied=20maxSpend\n-=20no=20blind?= =?UTF-8?q?=20payment=20retries=20on=20unknown=20settlement;=20errors=20su?= =?UTF-8?q?rfaced=20to=20caller\n-=20submissions=20surfaced=20for=20human?= =?UTF-8?q?=20review;=20never=20silently=20accepted/rejected\n-=20no=20pri?= =?UTF-8?q?vate=20keys=20/=20secrets=20requested,=20stored,=20logged,=20or?= =?UTF-8?q?=20committed\n\nTests:=20node=20--test=20(12=20passing)=20exerc?= =?UTF-8?q?ise=20every=20gate=20and=20a=20CLI=20round-trip.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcp_modules/taskmarket/README.md | 138 ++++++++++++++++++ mcp_modules/taskmarket/docs/api.md | 54 +++++++ .../taskmarket/examples/basic-usage.js | 34 +++++ mcp_modules/taskmarket/index.js | 50 +++++++ mcp_modules/taskmarket/package.json | 22 +++ mcp_modules/taskmarket/src/controller.js | 78 ++++++++++ mcp_modules/taskmarket/src/service.js | 24 +++ mcp_modules/taskmarket/src/taskmarket.js | 122 ++++++++++++++++ .../taskmarket/test/taskmarket.test.js | 116 +++++++++++++++ 9 files changed, 638 insertions(+) create mode 100644 mcp_modules/taskmarket/README.md create mode 100644 mcp_modules/taskmarket/docs/api.md create mode 100644 mcp_modules/taskmarket/examples/basic-usage.js create mode 100644 mcp_modules/taskmarket/index.js create mode 100644 mcp_modules/taskmarket/package.json create mode 100644 mcp_modules/taskmarket/src/controller.js create mode 100644 mcp_modules/taskmarket/src/service.js create mode 100644 mcp_modules/taskmarket/src/taskmarket.js create mode 100644 mcp_modules/taskmarket/test/taskmarket.test.js diff --git a/mcp_modules/taskmarket/README.md b/mcp_modules/taskmarket/README.md new file mode 100644 index 0000000..15d2fe8 --- /dev/null +++ b/mcp_modules/taskmarket/README.md @@ -0,0 +1,138 @@ +# Taskmarket MCP Module + +Delegate real work to **Taskmarket** from inside the Profullstack MCP server. This +module lets a user or agent *recognize that a request is better delegated to external +workers* and, with **explicit authorization**, create or discover a Taskmarket task +instead of repeatedly spending inference or forcing an unreliable solution. + +- **Browse** open Taskmarket tasks +- **Create** a funded Taskmarket task after showing exact description, reward, deadline, + deliverables, Base network, and a maximum spend — and only after **fresh, explicit user + authorization** +- **Track** a task's live status by ID +- **Retrieve submissions and present them for human review** — the module never silently + accepts or rejects work +- **Accept** a submission only after explicit confirmation (and only by a human reviewer) + +Target product: **Profullstack MCP server** (established, actively maintained, public repo +with an `mcp_modules/` extension system). This module is a *new* `taskmarket` extension that +the server did not previously have. + +## Why this is a real integration + +The module is a first-party `mcp_modules/taskmarket` package that registers HTTP routes and +agent tools on the official server. It shells out to the **official `taskmarket` CLI** +(first-party Taskmarket tooling) to perform every operation, so it behaves exactly like a +user would on the command line — no reimplemented protocol, no mock interface. + +## Security model (required by the integration bounty) + +- **No secrets handled here.** The module never requests, stores, logs, or commits private + keys, seed phrases, tokens, cookies, or other secrets. The `taskmarket` CLI reads the + operator's configured wallet from its own secure store. This module only ever passes + public task parameters to the CLI. +- **Explicit authorization gate.** Every fund-moving call — `createTask` (funds the reward) + and `acceptSubmission` (costs 0.001 USDC) — requires the caller to send `confirm: true`. + Without it the server refuses and returns `requireConfirmation: true`. The server never + spends on its own initiative. +- **Network + spending checks.** Tasks may only be created on **Base mainnet**; any other + `network` is rejected. `reward` must not exceed the caller-supplied `maxSpend` ceiling. +- **No blind retries.** If a CLI call fails (e.g. unknown settlement status), the error is + surfaced to the caller. The module never auto-retries a payment whose outcome is unknown. +- **Human-in-the-loop review.** Submissions are retrieved and returned for a human to read; + acceptance is a separate, confirmed call. Work is never silently auto-accepted. + +## Setup + +```bash +# prerequisites: Node >= 18, the official taskmarket CLI on PATH, a configured wallet +cd profullstack-mcp-server +npm install +# the module is auto-discovered from mcp_modules/taskmarket (see src/core/moduleLoader.js) +npm start +``` + +Configure the CLI (one time, on the host — not in this repo): + +```bash +taskmarket wallet status # confirms a configured Base wallet +``` + +Point the module at a specific binary if needed: + +```bash +export TASKMARKET_BIN=/usr/local/bin/taskmarket +``` + +## HTTP API + +| Method | Path | Purpose | Auth | +| ------ | ---- | ------- | ---- | +| GET | `/taskmarket` | Module info | none | +| GET | `/taskmarket/capabilities` | Capabilities | none | +| GET | `/taskmarket/tasks?limit=20&mode=bounty` | Browse tasks | none | +| GET | `/taskmarket/tasks/:id` | Task details / live status | none | +| POST | `/taskmarket/tasks` | **Create** a funded task | `confirm:true` | +| GET | `/taskmarket/tasks/:id/submissions` | List submissions (review) | none | +| POST | `/taskmarket/tasks/:id/submissions/:subId/accept` | **Accept** submission | `confirm:true` | + +### Create a task (explicit authorization) + +```bash +curl -X POST http://localhost:3000/taskmarket/tasks \ + -H 'content-type: application/json' \ + -d '{ + "confirm": true, + "description": "Build a landing page for our launch", + "reward": 5, + "durationHours": 48, + "mode": "bounty", + "network": "base", + "maxSpend": 5 + }' +# -> { "created": { "data": { "taskId": "0x...", "link": "https://taskmarket.dev/task/0x..." } } } +``` + +Omit `confirm` (or set it `false`) and the server answers: + +```json +{ "error": "Explicit user authorization required: send confirm:true to create a funded task.", + "requireConfirmation": true } +``` + +### Review submissions + +```bash +curl http://localhost:3000/taskmarket/tasks/0x.../submissions +# -> { "submissions": { "data": [ { "submissionId": "0xSUB1", "worker": "0xW1" }, ... ] } } +``` + +### Accept a reviewed submission (explicit authorization) + +```bash +curl -X POST http://localhost:3000/taskmarket/tasks/0x.../submissions/0xSUB1/accept \ + -H 'content-type: application/json' -d '{"confirm": true}' +``` + +## CLI usage (same flows, directly) + +```bash +taskmarket task list --limit 20 +taskmarket task get 0x... +taskmarket task submissions 0x... +``` + +## Run the tests + +```bash +npm test +``` + +The suite unit-tests every authorization/spending/network gate and exercises the CLI +round-trip against an injected fake binary (no real wallet, network, or funds required). + +## Repository & upstream + +- Upstream (target): https://github.com/profullstack/mcp-server +- Module path: `mcp_modules/taskmarket/` +- Taskmarket: https://taskmarket.dev/ · Docs: https://docs.taskmarket.dev/ diff --git a/mcp_modules/taskmarket/docs/api.md b/mcp_modules/taskmarket/docs/api.md new file mode 100644 index 0000000..b0d485a --- /dev/null +++ b/mcp_modules/taskmarket/docs/api.md @@ -0,0 +1,54 @@ +# Taskmarket Module — API Reference + +All routes are mounted by `register(app)` in `index.js`. JSON bodies use +`content-type: application/json`. + +## GET /taskmarket +Module status and version. + +```json +{ "module": "taskmarket", "status": "active", "network": "base", + "message": "Delegate work to Taskmarket — browse, create (authorized), and review submissions on Base." } +``` + +## GET /taskmarket/capabilities +Returns supported version, network, and the list of available tools. + +## GET /taskmarket/tasks +Browse open tasks. +- Query: `limit` (default 20), `mode` (optional: bounty|claim|pitch|benchmark|auction) +- Response: `{ "tasks": }` + +## GET /taskmarket/tasks/:id +Get a single task's details / live status. `:id` must be a `0x`-prefixed hex string. + +## POST /taskmarket/tasks +Create a funded task. **Requires `confirm: true`.** + +Body: +```json +{ + "confirm": true, + "description": "string (required)", + "reward": 5, + "durationHours": 48, + "mode": "bounty", + "visibility": "public", + "network": "base", + "maxSpend": 5 +} +``` +Validation (all enforced before any spend): +- `confirm === true` else `400 requireConfirmation` +- `network === "base"` else `400 Unsupported network` +- `reward > 0` and `reward <= maxSpend` else `400` +- `durationHours > 0` else `400` +Response: `{ "created": }` + +## GET /taskmarket/tasks/:id/submissions +List submissions for a task, returned verbatim for **human review**. Never auto-accepted. + +## POST /taskmarket/tasks/:id/submissions/:subId/accept +Accept a submission. **Requires `confirm: true`.** Costs 0.001 USDC. +- Body: `{ "confirm": true }` +- Response: `{ "accepted": }` diff --git a/mcp_modules/taskmarket/examples/basic-usage.js b/mcp_modules/taskmarket/examples/basic-usage.js new file mode 100644 index 0000000..db01eb0 --- /dev/null +++ b/mcp_modules/taskmarket/examples/basic-usage.js @@ -0,0 +1,34 @@ +/** + * Basic usage example for the Taskmarket module. + * + * Real CLI: node examples/basic-usage.js + * Injected fake CLI: TASKMARKET_BIN=./fake.sh node examples/basic-usage.js + * + * The demo never spends funds: createTask is shown with confirm:false so the + * authorization gate rejects it. Flip to true only with a funded, authorized wallet. + */ +import { listTasks, getTask, listSubmissions, createTask } from "../src/taskmarket.js"; + +async function main() { + console.log("== Browse open tasks =="); + const tasks = await listTasks({ limit: 5 }); + console.log(JSON.stringify(tasks, null, 2)); + + const firstId = tasks?.data?.tasks?.[0]?.id; + if (firstId) { + console.log("\n== Get task details =="); + console.log(JSON.stringify(await getTask(firstId), null, 2)); + + console.log("\n== List submissions for human review =="); + console.log(JSON.stringify(await listSubmissions(firstId), null, 2)); + } + + console.log("\n== Create a funded task (authorization gate) =="); + try { + await createTask({ description: "Demo task", reward: 1, durationHours: 24, network: "base", maxSpend: 1, confirm: false }); + } catch (e) { + console.log("Authorization gate worked as designed ->", e.message); + } +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/mcp_modules/taskmarket/index.js b/mcp_modules/taskmarket/index.js new file mode 100644 index 0000000..81bd35e --- /dev/null +++ b/mcp_modules/taskmarket/index.js @@ -0,0 +1,50 @@ +/** + * Taskmarket Module + * + * Delegate real work to Taskmarket from inside the Profullstack MCP server. + * Browse open tasks, create a funded task after explicit user authorization, + * and retrieve submissions for human review — all on Base mainnet via the + * official taskmarket CLI (first-party tooling; no secrets handled here). + */ + +import { logger } from "../../src/utils/logger.js"; +import { + listTasksHandler, getTaskHandler, createTaskHandler, + listSubmissionsHandler, acceptSubmissionHandler, capabilities, +} from "./src/controller.js"; +import { listCapabilities } from "./src/service.js"; + +export async function register(app) { + logger.info("Registering taskmarket module"); + + app.get("/taskmarket", (c) => c.json({ + module: "taskmarket", + status: "active", + message: "Delegate work to Taskmarket — browse, create (authorized), and review submissions on Base.", + version: listCapabilities().version, + network: listCapabilities().network, + })); + + app.get("/taskmarket/capabilities", capabilities); + app.get("/taskmarket/tasks", listTasksHandler); + app.get("/taskmarket/tasks/:id", getTaskHandler); + app.post("/taskmarket/tasks", createTaskHandler); + app.get("/taskmarket/tasks/:id/submissions", listSubmissionsHandler); + app.post("/taskmarket/tasks/:id/submissions/:subId/accept", acceptSubmissionHandler); + + app.get("/tools/taskmarket/info", (c) => c.json({ + name: "taskmarket", + description: + "Browse Taskmarket tasks, create a funded task with explicit user authorization, and review " + + "submissions for human approval — all on Base mainnet via first-party taskmarket CLI.", + examples: [ + "GET /taskmarket/tasks", + "GET /taskmarket/tasks/:id", + "POST /taskmarket/tasks {confirm:true, description, reward, durationHours, network:'base', maxSpend}", + "GET /taskmarket/tasks/:id/submissions", + ], + })); +} + +export { listCapabilities } from "./src/service.js"; +export const metadata = { version: "1.0.0" }; diff --git a/mcp_modules/taskmarket/package.json b/mcp_modules/taskmarket/package.json new file mode 100644 index 0000000..3d2c565 --- /dev/null +++ b/mcp_modules/taskmarket/package.json @@ -0,0 +1,22 @@ +{ + "name": "mcp-module-taskmarket", + "version": "1.0.0", + "description": "Taskmarket delegation module for the Profullstack MCP server — browse, create (authorized), and review submissions on Base via first-party taskmarket CLI", + "main": "index.js", + "type": "module", + "scripts": { + "test": "node --test test/" + }, + "keywords": [ + "mcp", + "module", + "taskmarket", + "task", + "bounty", + "base", + "delegation", + "agent" + ], + "license": "MIT", + "dependencies": {} +} diff --git a/mcp_modules/taskmarket/src/controller.js b/mcp_modules/taskmarket/src/controller.js new file mode 100644 index 0000000..3e72f73 --- /dev/null +++ b/mcp_modules/taskmarket/src/controller.js @@ -0,0 +1,78 @@ +/** + * HTTP handlers for the taskmarket module. + * + * Authorization model: every fund-moving action (create a task, accept a + * submission) requires the caller to send confirm:true. The server never + * spends on its own — it only forwards a deliberate, authorized request to the + * first-party taskmarket CLI. + */ +import { + listTasks, getTask, createTask, listSubmissions, acceptSubmission, listCapabilities, +} from "./service.js"; + +export async function listTasksHandler(c) { + try { + const limit = Number(c.req.query("limit") || 20); + const mode = c.req.query("mode") || null; + const data = await listTasks({ limit, mode }); + return c.json({ tasks: data }); + } catch (err) { + return c.json({ error: err.message }, 400); + } +} + +export async function getTaskHandler(c) { + try { + const taskId = c.req.param("id"); + const data = await getTask(taskId); + return c.json(data); + } catch (err) { + return c.json({ error: err.message }, 400); + } +} + +export async function createTaskHandler(c) { + try { + const params = await c.req.json(); + if (params.confirm !== true) { + return c.json( + { error: "Explicit user authorization required: send confirm:true to create a funded task.", + requireConfirmation: true }, 400); + } + const data = await createTask(params); + return c.json({ created: data }); + } catch (err) { + return c.json({ error: err.message }, 400); + } +} + +export async function listSubmissionsHandler(c) { + try { + const taskId = c.req.param("id"); + const data = await listSubmissions(taskId); + return c.json({ submissions: data }); + } catch (err) { + return c.json({ error: err.message }, 400); + } +} + +export async function acceptSubmissionHandler(c) { + try { + const taskId = c.req.param("id"); + const submissionId = c.req.param("subId"); + const body = await c.req.json().catch(() => ({})); + if (body.confirm !== true) { + return c.json( + { error: "Explicit user authorization required: send confirm:true to accept a submission.", + requireConfirmation: true }, 400); + } + const data = await acceptSubmission(taskId, submissionId, true); + return c.json({ accepted: data }); + } catch (err) { + return c.json({ error: err.message }, 400); + } +} + +export async function capabilities(c) { + return c.json(listCapabilities()); +} diff --git a/mcp_modules/taskmarket/src/service.js b/mcp_modules/taskmarket/src/service.js new file mode 100644 index 0000000..f160f6c --- /dev/null +++ b/mcp_modules/taskmarket/src/service.js @@ -0,0 +1,24 @@ +/** + * Taskmarket service — thin wrapper exposing the CLI-backed operations and + * capability metadata to the HTTP/tool layer. All safety gates (explicit + * confirmation, Base-only network, max-spend ceiling) live in src/taskmarket.js. + */ +import { + listTasks, getTask, createTask, listSubmissions, acceptSubmission, + TASKMARKET_MODULE_VERSION, SUPPORTED_NETWORK, +} from "./taskmarket.js"; + +export const TASKMARKET_VERSION = TASKMARKET_MODULE_VERSION; + +export function listCapabilities() { + return { + version: TASKMARKET_MODULE_VERSION, + network: SUPPORTED_NETWORK, + tools: ["listTasks", "getTask", "createTask", "listSubmissions", "acceptSubmission"], + notes: + "Uses first-party taskmarket CLI; no secrets stored. Mutating calls " + + "(createTask, acceptSubmission) require confirm:true from the caller.", + }; +} + +export { listTasks, getTask, createTask, listSubmissions, acceptSubmission }; diff --git a/mcp_modules/taskmarket/src/taskmarket.js b/mcp_modules/taskmarket/src/taskmarket.js new file mode 100644 index 0000000..04e5015 --- /dev/null +++ b/mcp_modules/taskmarket/src/taskmarket.js @@ -0,0 +1,122 @@ +/** + * Taskmarket module — first-party CLI wrapper. + * + * Uses the OFFICIAL `taskmarket` CLI as first-party Taskmarket tooling. + * No private keys, seed phrases, tokens, cookies, or secrets are requested, + * stored, logged, or committed: the CLI reads the operator's configured wallet + * from its own secure store. This module only shells out to the CLI and parses + * its JSON output. Every mutating call (create / accept) is gated on an explicit + * `confirm` flag so no funds move without fresh, deliberate user authorization. + */ + +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +// Binary resolved at call time so tests can inject a fake via TASKMARKET_BIN. +export const TASKMARKET_MODULE_VERSION = "1.0.0"; +export const SUPPORTED_NETWORK = "base"; // Base mainnet only — never blindly spend elsewhere + +function stripAnsi(s) { + return String(s).replace(/\x1b\[[0-9;]*m/g, ""); +} + +function extractJson(text) { + const clean = stripAnsi(text); + const s = clean.indexOf("{"); + const e = clean.lastIndexOf("}"); + if (s < 0 || e < 0 || e <= s) return null; + try { + return JSON.parse(clean.slice(s, e + 1)); + } catch { + return null; + } +} + +/** Run the taskmarket CLI and return parsed JSON (or {raw} when no JSON is emitted). */ +export async function runTaskmarket(args, { timeout = 120000 } = {}) { + const bin = process.env.TASKMARKET_BIN || "taskmarket"; + const { stdout, stderr } = await execFileAsync(bin, args, { + timeout, + maxBuffer: 16 * 1024 * 1024, + }); + const out = stripAnsi((stdout || "") + "\n" + (stderr || "")); + const data = extractJson(out); + return data === null ? { raw: out.trim() } : data; +} + +const HEX = /^0x[0-9a-fA-F]+$/; + +export async function listTasks({ limit = 20, mode = null } = {}) { + const args = ["task", "list", "--limit", String(limit)]; + if (mode) args.push("--mode", mode); + return runTaskmarket(args); +} + +export async function getTask(taskId) { + if (!HEX.test(taskId)) throw new Error("Invalid taskId"); + return runTaskmarket(["task", "get", taskId]); +} + +/** + * Create a Taskmarket task (costs `reward` USDC from the configured wallet). + * Requires explicit `confirm: true`. Enforces Base network + spending ceiling. + * Never retries blindly: any failure is surfaced to the caller. + */ +export async function createTask(params) { + const { description, reward, durationHours, mode = "bounty", visibility = "public", + network = "base", confirm, maxSpend } = params; + + if (confirm !== true) { + throw new Error("Explicit user authorization required: pass confirm:true to create a funded task."); + } + if (network !== SUPPORTED_NETWORK) { + throw new Error(`Unsupported network "${network}". This integration only operates on Base mainnet.`); + } + if (typeof description !== "string" || !description.trim()) { + throw new Error("description (non-empty string) is required"); + } + const rewardNum = Number(reward); + if (!Number.isFinite(rewardNum) || rewardNum <= 0) { + throw new Error("reward must be a positive USDC amount"); + } + const maxNum = maxSpend == null ? Infinity : Number(maxSpend); + if (!Number.isFinite(maxNum) || rewardNum > maxNum) { + throw new Error(`reward ${rewardNum} USDC exceeds authorized maxSpend ${maxNum} USDC`); + } + const dur = Number(durationHours); + if (!Number.isFinite(dur) || dur <= 0) { + throw new Error("durationHours must be a positive number"); + } + + const args = [ + "task", "create", + "--description", description, + "--reward", String(rewardNum), + "--duration", String(dur), + "--mode", mode, + "--task-visibility", visibility, + ]; + // Returns the created task id / link on success. No blind retry on failure. + return runTaskmarket(args); +} + +export async function listSubmissions(taskId) { + if (!HEX.test(taskId)) throw new Error("Invalid taskId"); + return runTaskmarket(["task", "submissions", taskId]); +} + +/** + * Accept a submission (costs 0.001 USDC). Requires explicit `confirm: true`. + * Presented only after human review — never silently auto-accepted. + */ +export async function acceptSubmission(taskId, submissionId, confirm) { + if (!HEX.test(taskId)) throw new Error("Invalid taskId"); + if (confirm !== true) { + throw new Error("Explicit user authorization required: pass confirm:true to accept a submission."); + } + const args = ["task", "accept", taskId]; + if (submissionId) args.push("--submission", String(submissionId)); + return runTaskmarket(args); +} diff --git a/mcp_modules/taskmarket/test/taskmarket.test.js b/mcp_modules/taskmarket/test/taskmarket.test.js new file mode 100644 index 0000000..60d9534 --- /dev/null +++ b/mcp_modules/taskmarket/test/taskmarket.test.js @@ -0,0 +1,116 @@ +/** + * Taskmarket module tests. + * + * Validation gates are unit-tested directly. The CLI round-trip is exercised + * against an injected fake binary (TASKMARKET_BIN) so the suite needs no real + * wallet, network, or funds. + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import assert from "node:assert"; +import { describe, it, before, after } from "node:test"; +import { + runTaskmarket, listTasks, getTask, createTask, listSubmissions, acceptSubmission, +} from "../src/taskmarket.js"; + +const fakeBin = path.join(os.tmpdir(), "fake-taskmarket-" + process.pid + ".sh"); + +before(function () { + fs.writeFileSync(fakeBin, + `#!/bin/bash +` + + `args="$*" +` + + `if echo "$args" | grep -q "task list"; then +` + + ` echo '{"data":{"tasks":[{"id":"0xAAA","reward":"1000000","mode":"bounty"}]}}' +` + + `elif echo "$args" | grep -q "task get"; then +` + + ` echo '{"data":{"id":"0xAAA","reward":"1000000","status":"open"}}' +` + + `elif echo "$args" | grep -q "task submissions"; then +` + + ` echo '{"data":[{"submissionId":"0xSUB1","worker":"0xW1"},{"submissionId":"0xSUB2","worker":"0xW2"}]}' +` + + `elif echo "$args" | grep -q "task create"; then +` + + ` echo '{"data":{"taskId":"0xBEEF","link":"https://taskmarket.dev/task/0xBEEF"}}' +` + + `elif echo "$args" | grep -q "task accept"; then +` + + ` echo '{"data":{"accepted":"0xSUB1"}}' +` + + `else +` + + ` echo '{"data":{}}' +` + + `fi +`); + fs.chmodSync(fakeBin, 0o755); + process.env.TASKMARKET_BIN = fakeBin; +}); + +after(function () { try { fs.unlinkSync(fakeBin); } catch {} }); + +describe("taskmarket CLI wrapper", () => { + it("parses JSON from CLI output (strips ANSI)", async () => { + const r = await runTaskmarket(["task", "list", "--limit", "1"]); + assert.ok(r.data && r.data.tasks && r.data.tasks.length === 1); + }); + + it("listTasks returns tasks", async () => { + const r = await listTasks({ limit: 5 }); + assert.strictEqual(r.data.tasks[0].id, "0xAAA"); + }); + + it("getTask returns task details for a valid id", async () => { + const r = await getTask("0xAAA"); + assert.strictEqual(r.data.id, "0xAAA"); + }); + + it("getTask rejects a malformed taskId", async () => { + await assert.rejects(() => getTask("not-hex"), /Invalid taskId/); + }); + + it("createTask REQUIRES explicit confirmation", async () => { + await assert.rejects(() => createTask({ description: "d", reward: 1, durationHours: 1 }), + /Explicit user authorization required/); + }); + + it("createTask rejects non-Base networks", async () => { + await assert.rejects(() => createTask({ description: "d", reward: 1, durationHours: 1, network: "eth", confirm: true }), + /Unsupported network/); + }); + + it("createTask enforces maxSpend ceiling", async () => { + await assert.rejects(() => createTask({ description: "d", reward: 5, durationHours: 1, network: "base", maxSpend: 2, confirm: true }), + /exceeds authorized maxSpend/); + }); + + it("createTask rejects non-positive reward", async () => { + await assert.rejects(() => createTask({ description: "d", reward: 0, durationHours: 1, network: "base", confirm: true }), + /positive USDC/); + }); + + it("createTask succeeds with confirm+base+spend and returns the task id", async () => { + const r = await createTask({ description: "Test task", reward: 1, durationHours: 2, network: "base", maxSpend: 10, confirm: true }); + assert.strictEqual(r.data.taskId, "0xBEEF"); + }); + + it("listSubmissions returns submissions for human review", async () => { + const r = await listSubmissions("0xAAA"); + assert.strictEqual(r.data.length, 2); + }); + + it("acceptSubmission REQUIRES explicit confirmation", async () => { + await assert.rejects(() => acceptSubmission("0xAAA", "0xSUB1", false), + /Explicit user authorization required/); + }); + + it("acceptSubmission succeeds with confirm", async () => { + const r = await acceptSubmission("0xAAA", "0xSUB1", true); + assert.strictEqual(r.data.accepted, "0xSUB1"); + }); +}); From 185ab233693978dbebb66b9eb72ea0db2e20aee1 Mon Sep 17 00:00:00 2001 From: "Nadine Roth (COO)" Date: Thu, 20 Aug 2026 18:05:15 +0200 Subject: [PATCH 2/2] test(taskmarket): use mkdtemp for the fake CLI binary (resolves CodeQL insecure-temp-file) --- mcp_modules/taskmarket/test/taskmarket.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mcp_modules/taskmarket/test/taskmarket.test.js b/mcp_modules/taskmarket/test/taskmarket.test.js index 60d9534..1daa46a 100644 --- a/mcp_modules/taskmarket/test/taskmarket.test.js +++ b/mcp_modules/taskmarket/test/taskmarket.test.js @@ -14,7 +14,8 @@ import { runTaskmarket, listTasks, getTask, createTask, listSubmissions, acceptSubmission, } from "../src/taskmarket.js"; -const fakeBin = path.join(os.tmpdir(), "fake-taskmarket-" + process.pid + ".sh"); +const fakeBinDir = fs.mkdtempSync(path.join(os.tmpdir(), "fake-taskmarket-")); +const fakeBin = path.join(fakeBinDir, "fake-taskmarket.sh"); before(function () { fs.writeFileSync(fakeBin, @@ -52,7 +53,7 @@ before(function () { process.env.TASKMARKET_BIN = fakeBin; }); -after(function () { try { fs.unlinkSync(fakeBin); } catch {} }); +after(function () { try { fs.rmSync(fakeBinDir, { recursive: true, force: true }); } catch {} }); describe("taskmarket CLI wrapper", () => { it("parses JSON from CLI output (strips ANSI)", async () => {