cascrow-mcp 1.8.0 → 3.1.0
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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +31 -25
- package/index.js +641 -154
- package/package.json +9 -3
- package/REMOTE_HTTP_SERVER.md +0 -97
- package/server.json +0 -46
- package/smithery.yaml +0 -28
package/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Cascrow MCP Server
|
|
4
|
-
* Gives
|
|
5
|
-
* submit proofs,
|
|
4
|
+
* Gives an agent the ability to verify work with cascrow's AI panel, create
|
|
5
|
+
* contracts, submit proofs, read on-chain reputation and — for a milestone with
|
|
6
|
+
* an amount — take part in a non-custodial MilestoneEscrowV3 escrow (offer,
|
|
7
|
+
* acceptance, unsigned transactions). It never signs or sends a transaction:
|
|
8
|
+
* signing happens in the separate local signer, cascrow-signer.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -28,6 +31,108 @@ function xrplTxUrl(hash) {
|
|
|
28
31
|
// IP-per-day tier as the web page). Every other tool needs a real account.
|
|
29
32
|
const ANONYMOUS_MODE = !API_KEY;
|
|
30
33
|
|
|
34
|
+
// Tools removed because they took a private key (2.0.0, cascrow Phase 5). The
|
|
35
|
+
// words are the server's own refusal (src/lib/custody-path-removed.ts), copied
|
|
36
|
+
// because a published package cannot import from the app;
|
|
37
|
+
// custody-removed-guards.test.ts holds the copy equal to the owner.
|
|
38
|
+
const REMOVED_CUSTODY_TOOLS = ["cascrow_escrow_fund"];
|
|
39
|
+
const CUSTODY_TOOL_REMOVED =
|
|
40
|
+
"Error: This way of funding has been removed: cascrow never accepts, creates, stores or signs with private keys. " +
|
|
41
|
+
"A $0 verification contract needs no funding — it is active once the builder has joined. " +
|
|
42
|
+
"A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP, funded by the payer with one transaction " +
|
|
43
|
+
"from their own machine: first link the wallet you will fund from (GET /api/user/wallet/challenge?address=<your wallet>, sign the message with that wallet, " +
|
|
44
|
+
"POST /api/user/wallet), then run the local signer: npx cascrow-signer@0.2.0 fund --milestone <milestoneId>. " +
|
|
45
|
+
"If your request contained a private key, treat it as exposed and move its funds to a new wallet. " +
|
|
46
|
+
"[CUSTODY_PATH_REMOVED]";
|
|
47
|
+
|
|
48
|
+
// What every cascrow client says about MilestoneEscrowV3 — a JSON copy of
|
|
49
|
+
// V3_CLIENT_TEXT in src/lib/v3-client-text.ts (this package cannot import the
|
|
50
|
+
// app). v3-client-text-parity.test.ts holds the copy equal to the owner.
|
|
51
|
+
// BEGIN v3-client-text
|
|
52
|
+
const V3_CLIENT_TEXT = {
|
|
53
|
+
"signerPin": "cascrow-signer@0.2.0",
|
|
54
|
+
"remoteTools": [
|
|
55
|
+
"cascrow_v3_offer",
|
|
56
|
+
"cascrow_v3_acceptance_get",
|
|
57
|
+
"cascrow_v3_acceptance_submit",
|
|
58
|
+
"cascrow_v3_prepare",
|
|
59
|
+
"cascrow_v3_report_tx",
|
|
60
|
+
"cascrow_v3_attestation",
|
|
61
|
+
"cascrow_v3_claims"
|
|
62
|
+
],
|
|
63
|
+
"codeHints": {
|
|
64
|
+
"V3_DISABLED": "MilestoneEscrowV3 is switched off for NEW money right now (offers, acceptances, funding). An escrow that already exists is never locked: object, release, refund, claim and withdraw keep working (cascrow_v3_prepare, or npx cascrow-signer@0.2.0 without cascrow at all).",
|
|
65
|
+
"RATE_UNAVAILABLE": "cascrow could not get a consistent XRP/USD rate from its two sources, so no offer can be priced. Nothing was created; retry in a minute.",
|
|
66
|
+
"SCREENING_UNAVAILABLE": "cascrow could not complete the sanctions screening this new-escrow step needs (offer, acceptance or funding), so nothing was created or prepared. This is a condition on cascrow's side, not a finding about you or your counterparty — retry in a few minutes. It does not touch escrows that already exist: their actions keep working (cascrow_v3_prepare, or npx cascrow-signer@0.2.0).",
|
|
67
|
+
"V3_NOT_OFFERED": "The payer has not offered MilestoneEscrowV3 terms for this milestone yet — only the payer can (cascrow_v3_offer). The payer has been notified.",
|
|
68
|
+
"OFFER_EXISTS": "An open offer already exists for this milestone and stays valid until it expires. Wait for the payee's acceptance or for the offer to lapse; sending the same inputs again returns the same offer.",
|
|
69
|
+
"V3_OFFER_OPEN": "An open MilestoneEscrowV3 offer, or a signed acceptance that is not funded yet, is bound to this milestone or wallet, so it cannot change now. Wait until the payer funds the escrow from their own machine (npx cascrow-signer@0.2.0 fund --milestone <milestoneId>) or the offer or acceptance lapses.",
|
|
70
|
+
"ATTESTATION_PENDING": "The AI panel has not finished on this claim. Its result is a non-binding recommendation — as payer you can object NOW, before the objection deadline, without waiting for it.",
|
|
71
|
+
"CLAIM_PROOF_UNKNOWN": "This claim's proofHash matches no evidence submitted to cascrow, so there is no AI recommendation for it. As payer, judge the delivery yourself and object before the objection deadline if it is not what was agreed.",
|
|
72
|
+
"V3_ESCROW": "This milestone is held in a MilestoneEscrowV3 escrow: only its payer, payee or arbiter moves that money, on chain, with their own key. Get the unsigned transaction with cascrow_v3_prepare, or sign locally with npx cascrow-signer@0.2.0.",
|
|
73
|
+
"V2_ESCROW_PRESENT": "This milestone or its contract still carries data of the retired v2 (RLUSD) escrow, so it cannot become a MilestoneEscrowV3 escrow. Nothing was changed; contact support.",
|
|
74
|
+
"INVALID_SIGNATURE": "The acceptance signature does not verify for your linked wallet over these exact terms (EIP-712, low-s, from an ordinary account). Sign the terms returned by cascrow_v3_acceptance_get again — npx cascrow-signer@0.2.0 accept does it and checks them first.",
|
|
75
|
+
"V3_INDEX_STALE": "cascrow's index of the chain is behind. Read the escrow from the chain directly (npx cascrow-signer@0.2.0 claims, or getEscrow / ClaimSubmitted); every deadline, above all an objection window, runs on chain regardless.",
|
|
76
|
+
"ACCEPTANCE_LAPSED": "The acceptance expired without funding, so the milestone is closed — this is not a failure of either party. A new attempt is a new milestone.",
|
|
77
|
+
"NOT_COMPLETABLE_WITHOUT_ESCROW": "The work was approved, but this milestone carries an amount without a MilestoneEscrowV3 escrow, and the previous escrow system has been removed — nothing can complete or pay it, so it was left as it was and NO money moved. cascrow has opened a case for it; contact support@cascrow.com with the contract ID. Do not resubmit: your evidence is recorded.",
|
|
78
|
+
"RELEASE_HOLD_REMOVED": "releaseHoldHours was removed with the previous escrow system; send the contract without it. A MilestoneEscrowV3 escrow's review period is agreed in the payer's offer (cascrow_v3_offer), and a $0 verification contract has no payout to delay.",
|
|
79
|
+
"AMOUNT_VIA_MILESTONES": "This draft has milestones, and their amounts are the contract's amount — it cannot be set on the contract. Nothing was changed. To change an amount, delete the draft and create the contract again; before joining, the invited builder can propose other amounts in a counter-proposal, which the requester can accept.",
|
|
80
|
+
"DEADLINE_VIA_MILESTONES": "This draft has milestones, and their deadlines are the ones enforced — a contract-level deadline changes nothing, so it is refused. Nothing was changed. To change a deadline, delete the draft and create the contract again; before joining, the invited builder can propose other deadlines in a counter-proposal, which the requester can accept.",
|
|
81
|
+
"MIXED_AMOUNTS": "A contract is either verification-only (every milestone $0) or escrowed (every milestone above $0), never both. Nothing was created or changed. Set every milestone to $0, or give every milestone an amount and fund each one through a MilestoneEscrowV3 offer (cascrow_v3_offer) once the builder has joined."
|
|
82
|
+
},
|
|
83
|
+
"joined": {
|
|
84
|
+
"active": "Joined as Builder. This is a $0 verification contract, so it is active now — submit proof for a milestone (cascrow_submit_proof). No money is held or moved.",
|
|
85
|
+
"awaitingOffer": "Joined as Builder. The milestones carry an amount, so nothing is funded yet: wait for the payer's MilestoneEscrowV3 offer, then accept it with your own wallet key — npx cascrow-signer@0.2.0 accept --milestone <milestoneId>, or on the contract page. cascrow never signs for you. Submit proof once the payer has funded the milestone.",
|
|
86
|
+
"unknown": "Joined as Builder. The server did not say whether the contract is active — check it with cascrow_get_contract before submitting proof."
|
|
87
|
+
},
|
|
88
|
+
"payeeHints": {
|
|
89
|
+
"CLAIM_PROOF_UNKNOWN": "This manifest is not a sealed submission of this milestone, so cascrow will not confirm it and nothing was signed. As payee, claim with the manifestHash the evidence door returned (the upload's answer, or cascrow_get_proof_status): npx cascrow-signer@0.2.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>."
|
|
90
|
+
},
|
|
91
|
+
"fundRemoved": "cascrow_fund_milestone was removed (and with it fundMilestone in cascrow-sdk and `cascrow fund`). A $0 verification contract is active by itself once the builder has joined — there is nothing to fund. A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP: the payer offers terms (cascrow_v3_offer), the builder accepts with their own key, and the payer funds it with one transaction from their own machine — npx cascrow-signer@0.2.0 fund --milestone <milestoneId>. cascrow never signs or sends for you.",
|
|
92
|
+
"recommendation": "🧭 AI RECOMMENDATION {verdict} for this MilestoneEscrowV3 submission ({confidence}% confidence on this file) — this verdict moves NO money. The submission is YES only when every file in it has a definite AI yes; only then is it claim-ready. As payee: claim on chain with the submission's manifestHash (npx cascrow-signer@0.2.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>). As payer: release, or object before the objection deadline.",
|
|
93
|
+
"noArbiterVeto": "No arbiter is named. The payer can object to every claim at no cost, and after the deadline an objected claim ends in a refund to the payer. This escrow protects you only against an inactive payer, not a dishonest one.",
|
|
94
|
+
"reportTxOnlyHash": "cascrow_v3_report_tx takes a transaction HASH (0x + 64 hex characters) and nothing else. cascrow never accepts a signed transaction — broadcast it yourself to the chain's public RPC."
|
|
95
|
+
};
|
|
96
|
+
// END v3-client-text
|
|
97
|
+
const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
98
|
+
function v3HintFor(code, body) {
|
|
99
|
+
if (!code) return null;
|
|
100
|
+
// Role-aware (post-review H9): CLAIM_PROOF_UNKNOWN without objectionDeadline comes from
|
|
101
|
+
// prepare?action=submitClaim and reaches the PAYEE; with it, a live claim — the payer's.
|
|
102
|
+
const b = body && typeof body === "object" ? body : null;
|
|
103
|
+
const liveClaim = !!b && typeof b.objectionDeadline === "string";
|
|
104
|
+
if (b && !liveClaim && V3_CLIENT_TEXT.payeeHints[code]) return V3_CLIENT_TEXT.payeeHints[code];
|
|
105
|
+
return V3_CLIENT_TEXT.codeHints[code] || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The fields of a V3 refusal body its recipient acts on, as one line (post-review G5) — the owner's rule. */
|
|
109
|
+
function v3RefusalDetail(body) {
|
|
110
|
+
if (!body || typeof body !== "object") return "";
|
|
111
|
+
const out = [];
|
|
112
|
+
if (typeof body.remedy === "string" && body.remedy) out.push(body.remedy);
|
|
113
|
+
if (Array.isArray(body.allowed)) out.push(`Allowed now: ${body.allowed.length ? body.allowed.map(String).join(", ") : "nothing"}.`);
|
|
114
|
+
if (typeof body.objectionDeadline === "string" && body.objectionDeadline) out.push(`Objection deadline: ${body.objectionDeadline}.`);
|
|
115
|
+
if (typeof body.offerExpiresAt === "string" && body.offerExpiresAt) out.push(`Open offer expires: ${body.offerExpiresAt}.`);
|
|
116
|
+
return out.join(" ");
|
|
117
|
+
}
|
|
118
|
+
// `submissionVerdict` is the SUBMISSION's verdict (YES / NO / PENDING), never the
|
|
119
|
+
// panel's `decision` on one file — a file at YES 70 % is a NO for the claim. A body
|
|
120
|
+
// without it reads as PENDING. Mirrors v3RecommendationLine in src/lib/v3-client-text.ts.
|
|
121
|
+
/**
|
|
122
|
+
* The line for a `POST /api/contracts/join` answer — same rule as `joinedLine` in
|
|
123
|
+
* src/lib/v3-client-text.ts (v3-client-text-parity.test.ts runs this copy against it).
|
|
124
|
+
*/
|
|
125
|
+
function joinedLine(body) {
|
|
126
|
+
const activated = body && typeof body === "object" ? body.activated : undefined;
|
|
127
|
+
if (activated === true) return V3_CLIENT_TEXT.joined.active;
|
|
128
|
+
if (activated === false) return V3_CLIENT_TEXT.joined.awaitingOffer;
|
|
129
|
+
return V3_CLIENT_TEXT.joined.unknown;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function v3RecommendationLine(submissionVerdict, confidence) {
|
|
133
|
+
return V3_CLIENT_TEXT.recommendation.replace("{verdict}", submissionVerdict ?? "PENDING").replace("{confidence}", String(confidence ?? 0));
|
|
134
|
+
}
|
|
135
|
+
|
|
31
136
|
if (ANONYMOUS_MODE) {
|
|
32
137
|
process.stderr.write(
|
|
33
138
|
"NOTE: CASCROW_API_KEY is not set — running in anonymous mode. " +
|
|
@@ -88,7 +193,7 @@ function retryAfterOf(res, data) {
|
|
|
88
193
|
* here: a single call either answers or throws; only the poll loop waits and asks
|
|
89
194
|
* again.
|
|
90
195
|
*/
|
|
91
|
-
async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT_ABORT_MS } = {}) {
|
|
196
|
+
async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT_ABORT_MS, headers = {} } = {}) {
|
|
92
197
|
const controller = new AbortController();
|
|
93
198
|
const timer = setTimeout(() => controller.abort(), deadlineMs);
|
|
94
199
|
let res;
|
|
@@ -96,7 +201,7 @@ async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT
|
|
|
96
201
|
try {
|
|
97
202
|
res = await fetch(`${BASE_URL}${path}`, {
|
|
98
203
|
method,
|
|
99
|
-
headers: { ...authHeader, ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
|
|
204
|
+
headers: { ...authHeader, ...headers, ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
|
|
100
205
|
...(json !== undefined ? { body: JSON.stringify(json) } : form !== undefined ? { body: form } : {}),
|
|
101
206
|
signal: controller.signal,
|
|
102
207
|
});
|
|
@@ -125,8 +230,9 @@ async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT
|
|
|
125
230
|
const retryAfterSeconds = retryAfterOf(res, data);
|
|
126
231
|
const base = typeof data?.error === "string" && data.error ? data.error : `HTTP ${res.status}`;
|
|
127
232
|
// A platform error page on a POST (typically the 504 when a route hits its own
|
|
128
|
-
// maxDuration —
|
|
129
|
-
// may have moved money before it was killed. Say so, and
|
|
233
|
+
// maxDuration — contracts/review settles on chain inside its own) does not mean the
|
|
234
|
+
// call failed: the function may have moved money before it was killed. Say so, and
|
|
235
|
+
// how to check.
|
|
130
236
|
const mayHaveCompleted = method !== "GET" && res.status >= 500 && data.code === "UPSTREAM_NON_JSON";
|
|
131
237
|
throw new CascrowApiError(
|
|
132
238
|
`${base}${code ? ` [${code}]` : ""}${retryAfterSeconds !== null && (res.status === 429 || res.status === 503) ? ` — retry after ${retryAfterSeconds} s` : ""}` +
|
|
@@ -157,8 +263,8 @@ async function apiGet(path) {
|
|
|
157
263
|
return fetchJson("GET", path);
|
|
158
264
|
}
|
|
159
265
|
|
|
160
|
-
async function apiPatch(path, body) {
|
|
161
|
-
return fetchJson("PATCH", path, { json: body });
|
|
266
|
+
async function apiPatch(path, body, headers) {
|
|
267
|
+
return fetchJson("PATCH", path, { json: body, headers });
|
|
162
268
|
}
|
|
163
269
|
|
|
164
270
|
async function uploadProof(milestoneId, filename, content) {
|
|
@@ -381,8 +487,26 @@ const TOOLS = [
|
|
|
381
487
|
type: "string",
|
|
382
488
|
enum: ["AUTO", "MANUAL", "MANUAL_AUTO"],
|
|
383
489
|
description:
|
|
384
|
-
"What happens when the AI panel is unsure (confidence 60–85). AUTO (default) rejects
|
|
385
|
-
"via cascrow_review_contract;
|
|
490
|
+
"What happens when the AI panel is unsure (confidence 60–85). AUTO (default) rejects: the panel decides alone (the same as panelDecidesAlone: true). " +
|
|
491
|
+
"MANUAL and MANUAL_AUTO put the milestone to your decision via cascrow_review_contract; since 2026-10-20 the two behave the same, and your window is reviewWindowDays. " +
|
|
492
|
+
"Without your decision a milestone completes automatically only when the panel's own verdict opened the review with a YES; any other review " +
|
|
493
|
+
"(a NO majority, a panel outage, a rejection overturned on appeal, an escalation after the builder used up their resubmissions) is handed to cascrow's human review instead — it never completes on silence. " +
|
|
494
|
+
"The window is counted from the email notice to you, never before the milestone was put to you (a panel verdict, an escalation after the builder used up their resubmissions, or a rejection overturned on appeal); without a sent notice it does not start. " +
|
|
495
|
+
"Contracts without an amount only, and nothing moves: a milestone completed this way reports decidedBy \"sla\".",
|
|
496
|
+
},
|
|
497
|
+
reviewWindowDays: {
|
|
498
|
+
type: "integer",
|
|
499
|
+
minimum: 2,
|
|
500
|
+
maximum: 30,
|
|
501
|
+
description:
|
|
502
|
+
"Your review window in days (default 14). Below 7 days only when both you and the builder registered as businesses — " +
|
|
503
|
+
"otherwise the contract is refused (REVIEW_WINDOW_NEEDS_BUSINESS). The builder sees it before joining.",
|
|
504
|
+
},
|
|
505
|
+
panelDecidesAlone: {
|
|
506
|
+
type: "boolean",
|
|
507
|
+
description:
|
|
508
|
+
"true: the panel decides alone — a YES above 85 % completes, anything else is a rejection the builder can resubmit against; you do not review. " +
|
|
509
|
+
"Defaults to agentReviewMode === \"AUTO\"; contradicting agentReviewMode is refused (REVIEW_MODE_CONFLICT). The builder sees it before joining.",
|
|
386
510
|
},
|
|
387
511
|
milestones: {
|
|
388
512
|
type: "array",
|
|
@@ -425,7 +549,7 @@ const TOOLS = [
|
|
|
425
549
|
},
|
|
426
550
|
amountUSD: {
|
|
427
551
|
type: "number",
|
|
428
|
-
description: "Optional
|
|
552
|
+
description: "Optional. Omit or set to 0 for a verification-only contract: it is active by itself once the builder has joined, and no money moves. An amount above 0 makes the milestone a MilestoneEscrowV3 escrow in native XRP — the payer offers terms with cascrow_v3_offer, the builder accepts, and the payer funds it from their own machine (npx cascrow-signer@0.2.0 fund).",
|
|
429
553
|
},
|
|
430
554
|
},
|
|
431
555
|
},
|
|
@@ -433,41 +557,6 @@ const TOOLS = [
|
|
|
433
557
|
},
|
|
434
558
|
},
|
|
435
559
|
},
|
|
436
|
-
{
|
|
437
|
-
name: "cascrow_fund_milestone",
|
|
438
|
-
description:
|
|
439
|
-
"Fund (activate) a milestone so it can receive proof. " +
|
|
440
|
-
"Call this after creating the contract and before submitting proof for a milestone.",
|
|
441
|
-
inputSchema: {
|
|
442
|
-
type: "object",
|
|
443
|
-
required: ["contractId"],
|
|
444
|
-
properties: {
|
|
445
|
-
contractId: { type: "string", description: "The contract ID returned by cascrow_create_contract" },
|
|
446
|
-
milestoneId: { type: "string", description: "Optional specific milestone ID. If omitted, funds the first available milestone." },
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: "cascrow_escrow_fund",
|
|
452
|
-
description:
|
|
453
|
-
"Fund a milestone with REAL on-chain RLUSD escrow using the agent's own EVM private key. " +
|
|
454
|
-
"This does a real approve + fundMilestone transaction on the XRPL EVM Sidechain — " +
|
|
455
|
-
"funds are locked in the smart contract and released automatically after AI verification. " +
|
|
456
|
-
"Use this instead of cascrow_fund_milestone when you want real escrow (not just verification).",
|
|
457
|
-
inputSchema: {
|
|
458
|
-
type: "object",
|
|
459
|
-
// `amountUSD` was REQUIRED and is not read by the route — the escrowed
|
|
460
|
-
// amount is the milestone's own. See the same correction in
|
|
461
|
-
// src/app/api/mcp/route.ts.
|
|
462
|
-
required: ["contractId", "agentPrivateKey"],
|
|
463
|
-
properties: {
|
|
464
|
-
contractId: { type: "string", description: "The contract ID to fund" },
|
|
465
|
-
agentPrivateKey: { type: "string", description: "The agent's EVM private key (hex, 0x-prefixed). The agent's wallet must hold enough RLUSD." },
|
|
466
|
-
// Intentionally absent: the route ignores a body amountUSD.
|
|
467
|
-
milestoneId: { type: "string", description: "Optional specific milestone ID. If omitted, funds the first available milestone." },
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
560
|
{
|
|
472
561
|
name: "cascrow_submit_proof",
|
|
473
562
|
description:
|
|
@@ -498,7 +587,7 @@ const TOOLS = [
|
|
|
498
587
|
name: "cascrow_verify",
|
|
499
588
|
description:
|
|
500
589
|
"Start (or re-run) AI verification for a proof and return the result. " +
|
|
501
|
-
"5 AI models vote in parallel (Claude, GPT-4o, Gemini, Mistral, Cerebras). " +
|
|
590
|
+
"5 AI models vote in parallel (Claude, GPT-4o-mini, Gemini, Mistral, Cerebras). " +
|
|
502
591
|
"Returns decision, confidence score, action (VERIFIED/PENDING_REVIEW/REJECTED), and per-model votes. " +
|
|
503
592
|
"Upload already starts verification by itself: for a proof you just submitted use cascrow_watch_verification { proofId } instead. " +
|
|
504
593
|
"If a verification of this proof is already running, this call waits for it rather than failing. " +
|
|
@@ -527,7 +616,9 @@ const TOOLS = [
|
|
|
527
616
|
description:
|
|
528
617
|
"Join an existing contract as the Builder (the party that delivers the work). " +
|
|
529
618
|
"Use this when another agent or user has created a contract and shared an invite code with you. " +
|
|
530
|
-
"
|
|
619
|
+
"The answer says whether the contract is active now (activated: true — a $0 verification contract; submit proof right away) " +
|
|
620
|
+
"or waits for the payer's MilestoneEscrowV3 offer (activated: false — accept it with your own wallet key: " +
|
|
621
|
+
"npx cascrow-signer@0.2.0 accept --milestone <id>, then submit proof once the payer has funded it).",
|
|
531
622
|
inputSchema: {
|
|
532
623
|
type: "object",
|
|
533
624
|
required: ["inviteCode"],
|
|
@@ -579,7 +670,8 @@ const TOOLS = [
|
|
|
579
670
|
name: "cascrow_get_reputation",
|
|
580
671
|
description:
|
|
581
672
|
"Look up the on-chain reputation of any agent by their EVM wallet address. " +
|
|
582
|
-
"Returns milestones completed, milestones failed, success rate and
|
|
673
|
+
"Returns milestones completed, milestones failed, success rate and the value released to them — " +
|
|
674
|
+
"totalReleasedUsd, plus totalXrpReleased for MilestoneEscrowV3 escrows (totalRlusdReleased is deprecated: the retired v2 escrow only) — " +
|
|
583
675
|
"counted over EVERY eligible milestone. " +
|
|
584
676
|
"The `milestones` array is different: it carries only the records the builder has " +
|
|
585
677
|
"published (per-milestone opt-in, default private), each with its XRPL tx hashes as " +
|
|
@@ -628,15 +720,26 @@ const TOOLS = [
|
|
|
628
720
|
name: "cascrow_set_discoverable",
|
|
629
721
|
description:
|
|
630
722
|
"Configure this agent's discovery settings: opt in/out of the agent marketplace, " +
|
|
723
|
+
"whether its name is shown on the public check page of its certificates, " +
|
|
631
724
|
"set skill tags so Requester agents can find you, and register a callback URL to receive " +
|
|
632
725
|
"real-time events instead of polling (handoff received, milestone funded, verified, rejected). " +
|
|
633
|
-
"
|
|
726
|
+
"Turning discoverable or showNameOnCertificates ON publishes the account holder's name and needs " +
|
|
727
|
+
"their consent: call without consentVersion first — nothing changes, and the answer carries the exact " +
|
|
728
|
+
"text to show them and the consentVersion to send if they agree. Turning either off needs nothing.",
|
|
634
729
|
inputSchema: {
|
|
635
730
|
type: "object",
|
|
636
731
|
properties: {
|
|
637
732
|
discoverable: {
|
|
638
733
|
type: "boolean",
|
|
639
|
-
description: "Set to true to appear in cascrow_discover_agents searches",
|
|
734
|
+
description: "Set to true to appear in cascrow_discover_agents searches (needs consentVersion, see above)",
|
|
735
|
+
},
|
|
736
|
+
showNameOnCertificates: {
|
|
737
|
+
type: "boolean",
|
|
738
|
+
description: "Set to true to show the company name (or name) as \u201cDelivered by\u201d on the public check page of this account's certificates (needs consentVersion, see above)",
|
|
739
|
+
},
|
|
740
|
+
consentVersion: {
|
|
741
|
+
type: "string",
|
|
742
|
+
description: "The consent version the server named, sent only after the account holder agreed to the text it returned",
|
|
640
743
|
},
|
|
641
744
|
skills: {
|
|
642
745
|
type: "array",
|
|
@@ -645,11 +748,25 @@ const TOOLS = [
|
|
|
645
748
|
},
|
|
646
749
|
callbackUrl: {
|
|
647
750
|
type: "string",
|
|
648
|
-
description: "HTTPS URL to receive real-time event notifications (POST). Set to null to disable.",
|
|
751
|
+
description: "HTTPS URL to receive real-time event notifications (POST, signed — see cascrow_rotate_callback_secret). Set to null to disable.",
|
|
649
752
|
},
|
|
650
753
|
},
|
|
651
754
|
},
|
|
652
755
|
},
|
|
756
|
+
{
|
|
757
|
+
name: "cascrow_rotate_callback_secret",
|
|
758
|
+
description:
|
|
759
|
+
"Replace this agent's callback signing secret and return the new one. Callbacks are signed " +
|
|
760
|
+
"X-Cascrow-Signature: sha256=HMAC-SHA256(secret, \"t=\" + <unix ms of X-Cascrow-Timestamp> + \".\" + <raw body>). " +
|
|
761
|
+
"The previous secret stops verifying immediately, and the new secret enters this conversation. " +
|
|
762
|
+
"Call without confirm first: nothing changes and the answer is the warning; call again with confirm true to rotate.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
type: "object",
|
|
765
|
+
properties: {
|
|
766
|
+
confirm: { type: "boolean", description: "true only after reading the warning a call without confirm returns." },
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
653
770
|
{
|
|
654
771
|
name: "cascrow_list_my_contracts",
|
|
655
772
|
description:
|
|
@@ -673,17 +790,110 @@ const TOOLS = [
|
|
|
673
790
|
description:
|
|
674
791
|
"As the FUNDER, list the contracts on which you still owe a manual verdict. " +
|
|
675
792
|
"This is the pull behind the manual_review.required webhook: a webhook you missed is a decision " +
|
|
676
|
-
"that never gets made
|
|
677
|
-
"
|
|
678
|
-
"Each entry carries reviewMilestoneId; pass it to cascrow_review_contract."
|
|
793
|
+
"that never gets made — cascrow completes an undecided milestone when its review window closes: 48 hours under review mode MANUAL_AUTO or AUTO, 14 days otherwise. " +
|
|
794
|
+
"These are verification-only ($0) contracts: that completion moves no money. Poll this whenever you have work in flight — after a restart above all. " +
|
|
795
|
+
"Each entry carries reviewMilestoneId; pass it to cascrow_review_contract. " +
|
|
796
|
+
"MilestoneEscrowV3 claims are NOT decided here: when v3ClaimsPending is above 0, list them with cascrow_v3_claims " +
|
|
797
|
+
"and object on chain before each objectionDeadline — cascrow cannot object for you.",
|
|
679
798
|
inputSchema: { type: "object", properties: {} },
|
|
680
799
|
},
|
|
800
|
+
{
|
|
801
|
+
name: "cascrow_v3_claims",
|
|
802
|
+
description:
|
|
803
|
+
"As the PAYER of a MilestoneEscrowV3 escrow, list the claims that release your escrow to the builder unless YOUR wallet " +
|
|
804
|
+
"calls object() on chain before objectionDeadline. This is the pull behind the claim.submitted webhook — a veto with a " +
|
|
805
|
+
"clock must never depend on one delivery. cascrow cannot object for you: it holds no key of yours and relays nothing. " +
|
|
806
|
+
"objectable=false means the window has closed. If the index is not current the call fails and says so; then read " +
|
|
807
|
+
"getEscrow / ClaimSubmitted from the chain directly.",
|
|
808
|
+
inputSchema: { type: "object", properties: {} },
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "cascrow_v3_offer",
|
|
812
|
+
description:
|
|
813
|
+
"As the PAYER, offer MilestoneEscrowV3 terms for a milestone (native XRP, one escrow per milestone). cascrow prices it from two XRP/USD sources, freezes the terms " +
|
|
814
|
+
"and notifies the builder, who accepts with their own key. reviewPeriodSec is your objection window after a claim: 24 h only with watcherDeclared=true " +
|
|
815
|
+
"(you declare that you run a watcher, e.g. npx cascrow-signer@0.2.0 watch), otherwise at least 72 h; a first job with a new builder gets 7 days. " +
|
|
816
|
+
"An arbiter is optional — without one an objection costs nothing and the money returns to you after the deadline. Answers OFFER_EXISTS with the open offer's expiry.",
|
|
817
|
+
inputSchema: {
|
|
818
|
+
type: "object",
|
|
819
|
+
required: ["milestoneId", "reviewPeriodSec", "watcherDeclared"],
|
|
820
|
+
properties: {
|
|
821
|
+
milestoneId: { type: "string" },
|
|
822
|
+
reviewPeriodSec: { type: "integer", minimum: 3600, description: "Objection window after each claim, in seconds." },
|
|
823
|
+
watcherDeclared: { type: "boolean", description: "true only if you run a watcher that objects in time; allows a 24 h window." },
|
|
824
|
+
arbiter: { type: "string", description: "Optional arbiter address (0x…). Never cascrow, never a party." },
|
|
825
|
+
arbitrationPeriodSec: { type: "integer", description: "With an arbiter: how long they have to decide after an objection." },
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "cascrow_v3_acceptance_get",
|
|
831
|
+
description:
|
|
832
|
+
"As the BUILDER (payee), read the payer's frozen MilestoneEscrowV3 offer: the EIP-712 typed data to sign, escrowId, payer, amount in wei, the rate snapshot and acceptBy. " +
|
|
833
|
+
"Check every term yourself before signing — your signature binds you until acceptBy and cascrow cannot revoke it. " +
|
|
834
|
+
"npx cascrow-signer@0.2.0 accept checks it against its own pinned chain and contract and signs locally. Answers V3_NOT_OFFERED while the payer has not offered. " +
|
|
835
|
+
`If the offer names no arbiter (arbiter null; the zero address in the typed data), tell the builder before signing: ${V3_CLIENT_TEXT.noArbiterVeto}`,
|
|
836
|
+
inputSchema: { type: "object", required: ["milestoneId"], properties: { milestoneId: { type: "string" } } },
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: "cascrow_v3_acceptance_submit",
|
|
840
|
+
description:
|
|
841
|
+
"As the BUILDER, hand cascrow your acceptance signature (EIP-712 over exactly the terms from cascrow_v3_acceptance_get, signed by your linked wallet). " +
|
|
842
|
+
"This is the only signature of yours cascrow ever receives besides the wallet-link one. The payer is then notified to fund before acceptBy.",
|
|
843
|
+
inputSchema: {
|
|
844
|
+
type: "object",
|
|
845
|
+
required: ["offerId", "signature"],
|
|
846
|
+
properties: { offerId: { type: "string" }, signature: { type: "string", description: "0x-prefixed 65-byte EIP-712 signature." } },
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
name: "cascrow_v3_prepare",
|
|
851
|
+
description:
|
|
852
|
+
"Get the UNSIGNED MilestoneEscrowV3 transaction for an action: { chainId, to, value, data, call: { fn, args }, escrowId, action, role, expect }. " +
|
|
853
|
+
"cascrow never signs, sends or relays it. Before signing, rebuild data from the contract ABI yourself and check to/chainId against your OWN configuration — " +
|
|
854
|
+
"npx cascrow-signer@0.2.0 does all of that and sends to the public RPC. After funding, object/release/refund/claim never depend on this tool: the escrow can be moved from the chain alone.",
|
|
855
|
+
inputSchema: {
|
|
856
|
+
type: "object",
|
|
857
|
+
required: ["milestoneId", "action"],
|
|
858
|
+
properties: {
|
|
859
|
+
milestoneId: { type: "string" },
|
|
860
|
+
action: { type: "string", enum: ["fund", "submitClaim", "claim", "release", "object", "refund", "extendDeadline", "returnFunds", "withdraw"] },
|
|
861
|
+
proofHash: { type: "string", description: "submitClaim: the manifest hash of your evidence (0x…)." },
|
|
862
|
+
reasonHash: { type: "string", description: "object: keccak256 of your objection reason (0x…)." },
|
|
863
|
+
newDeadline: { type: "string", description: "extendDeadline: unix seconds." },
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
name: "cascrow_v3_report_tx",
|
|
869
|
+
description:
|
|
870
|
+
"After YOU broadcast a MilestoneEscrowV3 transaction, tell cascrow its hash so the index re-reads the chain now instead of at the next scheduled read (up to 30 minutes). " +
|
|
871
|
+
"Takes a transaction HASH only — never a signed transaction; cascrow relays nothing. Reporting is optional: the index reads the chain anyway.",
|
|
872
|
+
inputSchema: {
|
|
873
|
+
type: "object",
|
|
874
|
+
required: ["milestoneId", "txHash"],
|
|
875
|
+
properties: { milestoneId: { type: "string" }, txHash: { type: "string", description: "0x + 64 hex characters." } },
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
name: "cascrow_v3_attestation",
|
|
880
|
+
description:
|
|
881
|
+
"Read cascrow's signed AI attestation for a MilestoneEscrowV3 claim — a non-binding RECOMMENDATION (verified, confidence, proofHash, reportHash), never a release. " +
|
|
882
|
+
"ATTESTATION_PENDING means the panel is still running: as payer you can object now. CLAIM_PROOF_UNKNOWN means the claim's proofHash matches no evidence cascrow saw.",
|
|
883
|
+
inputSchema: {
|
|
884
|
+
type: "object",
|
|
885
|
+
required: ["escrowId", "claimIndex"],
|
|
886
|
+
properties: { escrowId: { type: "string" }, claimIndex: { type: "integer", minimum: 1 } },
|
|
887
|
+
},
|
|
888
|
+
},
|
|
681
889
|
{
|
|
682
890
|
name: "cascrow_review_contract",
|
|
683
891
|
description:
|
|
684
892
|
"As the FUNDER, approve or reject a milestone the AI panel could not decide on its own " +
|
|
685
|
-
"(confidence 60–85 lands in PENDING_REVIEW). APPROVE
|
|
893
|
+
"(confidence 60–85 lands in PENDING_REVIEW). APPROVE completes the milestone; " +
|
|
686
894
|
"REJECT hands the milestone back so they can resubmit stronger evidence — it is not terminal. " +
|
|
895
|
+
"This decides verification-only ($0) contracts and moves no money: a MilestoneEscrowV3 escrow is released or objected to " +
|
|
896
|
+
"on chain by the payer's own key (cascrow_v3_prepare, or npx cascrow-signer@0.2.0). " +
|
|
687
897
|
"Call cascrow_review_queue first: it names the contractId and the milestoneId. " +
|
|
688
898
|
"milestoneId is required once more than one milestone of that contract is pending.",
|
|
689
899
|
inputSchema: {
|
|
@@ -691,11 +901,40 @@ const TOOLS = [
|
|
|
691
901
|
required: ["contractId", "decision"],
|
|
692
902
|
properties: {
|
|
693
903
|
contractId: { type: "string", description: "The contract holding the milestone under review." },
|
|
694
|
-
decision: { type: "string", enum: ["APPROVE", "REJECT"], description: "APPROVE
|
|
904
|
+
decision: { type: "string", enum: ["APPROVE", "REJECT"], description: "APPROVE completes the milestone; REJECT returns it to the builder." },
|
|
695
905
|
milestoneId: { type: "string", description: "Required when more than one milestone of the contract is pending review." },
|
|
696
906
|
},
|
|
697
907
|
},
|
|
698
908
|
},
|
|
909
|
+
{
|
|
910
|
+
name: "cascrow_request_human_review",
|
|
911
|
+
description:
|
|
912
|
+
"As the funder OR the builder of a contract, ask for a person at cascrow to review a verification outcome (Art. 22(3) GDPR) — at any time, on any milestone the panel has decided. " +
|
|
913
|
+
"A person decides within one month (dueAt) and both parties receive the decision with its reasons (webhook human_review.decided; read it with cascrow_human_reviews). " +
|
|
914
|
+
"On a verification-only ($0) contract the decision can complete the milestone, reject it, or revoke a completion and withdraw its certificate; on a MilestoneEscrowV3 milestone it is a statement and changes nothing on chain. " +
|
|
915
|
+
"One open request per milestone: asking again while one is open answers ALREADY_OPEN with its dueAt. milestoneId is required once the contract has more than one milestone. " +
|
|
916
|
+
"reason is your own words to the reviewer (at most 2000 characters).",
|
|
917
|
+
inputSchema: {
|
|
918
|
+
type: "object",
|
|
919
|
+
required: ["contractId"],
|
|
920
|
+
properties: {
|
|
921
|
+
contractId: { type: "string" },
|
|
922
|
+
milestoneId: { type: "string", description: "Required once the contract has more than one milestone." },
|
|
923
|
+
reason: { type: "string", maxLength: 2000, description: "Why you ask for a review — shown to the reviewer only, never to the other party." },
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: "cascrow_human_reviews",
|
|
929
|
+
description:
|
|
930
|
+
"The human-review requests on a contract you are a party to, newest first: status (OPEN / DECIDED / CLOSED), dueAt, outcome and the reviewer's statement (decisionNote, which the reviewer wrote for both parties). " +
|
|
931
|
+
"Your own reason is returned to you only. The pull side of the human_review.requested / human_review.decided webhooks.",
|
|
932
|
+
inputSchema: {
|
|
933
|
+
type: "object",
|
|
934
|
+
required: ["contractId"],
|
|
935
|
+
properties: { contractId: { type: "string" } },
|
|
936
|
+
},
|
|
937
|
+
},
|
|
699
938
|
{
|
|
700
939
|
name: "cascrow_get_proof_status",
|
|
701
940
|
description:
|
|
@@ -717,7 +956,7 @@ const TOOLS = [
|
|
|
717
956
|
{
|
|
718
957
|
name: "cascrow_mcp_submit",
|
|
719
958
|
description:
|
|
720
|
-
"Submit text-based proof and get a verdict in the same call — the 5-model AI panel runs synchronously and takes up to ~100 seconds,
|
|
959
|
+
"Submit text-based proof and get a verdict in the same call — the 5-model AI panel runs synchronously and takes up to ~100 seconds, so allow at least 4 minutes (the MCP default request timeout of 60 s is too short); a 409 saying an earlier submission is still being processed is usually your own timed-out call — wait the seconds it names and retry, your evidence was kept; a 409 with code V3_ESCROW is final — the milestone is held in a MilestoneEscrowV3 escrow that only its payer, payee or arbiter moves, on chain — do not retry. " +
|
|
721
960
|
"On approval it automatically releases funds on-chain and mints an NFT certificate. " +
|
|
722
961
|
"Use this instead of cascrow_submit_proof + cascrow_verify when your proof is text/code (no file upload needed). " +
|
|
723
962
|
"Returns verdict ('approved' | 'rejected' | 'pending_review'), confidence score, and on-chain proof URL.",
|
|
@@ -842,7 +1081,7 @@ const TOOLS = [
|
|
|
842
1081
|
|
|
843
1082
|
// ─── Tool handlers ────────────────────────────────────────────────────────────
|
|
844
1083
|
|
|
845
|
-
async function handleCreateContract({ milestones, agentReviewMode }) {
|
|
1084
|
+
async function handleCreateContract({ milestones, agentReviewMode, reviewWindowDays, panelDecidesAlone }) {
|
|
846
1085
|
const deadline = (days) => {
|
|
847
1086
|
const d = new Date();
|
|
848
1087
|
d.setDate(d.getDate() + (days ?? 7));
|
|
@@ -870,6 +1109,9 @@ async function handleCreateContract({ milestones, agentReviewMode }) {
|
|
|
870
1109
|
};
|
|
871
1110
|
}),
|
|
872
1111
|
...(agentReviewMode !== undefined ? { agentReviewMode } : {}),
|
|
1112
|
+
// The review terms — validated by the server (review-terms.ts).
|
|
1113
|
+
...(reviewWindowDays !== undefined ? { reviewWindowDays } : {}),
|
|
1114
|
+
...(panelDecidesAlone !== undefined ? { panelDecidesAlone } : {}),
|
|
873
1115
|
};
|
|
874
1116
|
|
|
875
1117
|
const data = await apiPost("/api/contracts", payload);
|
|
@@ -884,35 +1126,12 @@ async function handleCreateContract({ milestones, agentReviewMode }) {
|
|
|
884
1126
|
inviteLink: inviteCode,
|
|
885
1127
|
contractUrl,
|
|
886
1128
|
milestoneCount: milestones.length,
|
|
1129
|
+
// What the builder will see before joining (window, who decides) — the server's own words.
|
|
1130
|
+
reviewTerms: data.reviewTerms ?? null,
|
|
887
1131
|
message: `Contract created with ${milestones.length} milestone(s). View it here: ${contractUrl}`,
|
|
888
1132
|
};
|
|
889
1133
|
}
|
|
890
1134
|
|
|
891
|
-
async function handleFundMilestone({ contractId, milestoneId }) {
|
|
892
|
-
const body = milestoneId ? { contractId, milestoneId } : { contractId };
|
|
893
|
-
const data = await apiPost("/api/agent/fund-milestone", body);
|
|
894
|
-
return {
|
|
895
|
-
milestoneId: data.milestoneId,
|
|
896
|
-
contractId: data.contractId,
|
|
897
|
-
status: data.status,
|
|
898
|
-
message: `Milestone funded and active. Ready to receive proof.`,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
async function handleEscrowFund({ contractId, agentPrivateKey, milestoneId }) {
|
|
903
|
-
// No amountUSD: the route funds the milestone's own figure and drops any body
|
|
904
|
-
// field silently. Forwarding it made the lie look answered.
|
|
905
|
-
const data = await apiPost("/api/agent/escrow-fund", { contractId, agentPrivateKey, milestoneId });
|
|
906
|
-
return {
|
|
907
|
-
milestoneId: data.milestoneId,
|
|
908
|
-
contractId: data.contractId,
|
|
909
|
-
txHash: data.txHash,
|
|
910
|
-
amountUSD: data.amountUSD,
|
|
911
|
-
status: data.status,
|
|
912
|
-
message: `✅ Real escrow funded on-chain. ${data.amountUSD} RLUSD locked in smart contract. TX: ${data.txHash}`,
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
1135
|
async function handleSubmitProof({ milestoneId, proof, filename }) {
|
|
917
1136
|
const data = await uploadProof(
|
|
918
1137
|
milestoneId,
|
|
@@ -968,8 +1187,10 @@ async function handleVerify({ proofId }) {
|
|
|
968
1187
|
// `passed: true` told an agent the escrow settled when it did not — the worst
|
|
969
1188
|
// direction of error for the one consumer with no in-app inbox.
|
|
970
1189
|
const complianceBlocked = result.complianceBlocked === true;
|
|
971
|
-
//
|
|
972
|
-
|
|
1190
|
+
// The panel approved a milestone that carries an amount without a MilestoneEscrowV3
|
|
1191
|
+
// escrow, so nothing completed it (Phase 6). Its own action, and a refusal: before it
|
|
1192
|
+
// existed this arrived as a bare VERIFIED and read as "the payout failed, retry".
|
|
1193
|
+
const notCompletable = action === "NOT_COMPLETABLE_WITHOUT_ESCROW";
|
|
973
1194
|
// A dead panel and a genuinely borderline verdict rendered identically, and an
|
|
974
1195
|
// agent's only sensible response to the second (improve the evidence, resubmit) is
|
|
975
1196
|
// wasted effort against the first.
|
|
@@ -1007,8 +1228,13 @@ async function handleVerify({ proofId }) {
|
|
|
1007
1228
|
: "";
|
|
1008
1229
|
|
|
1009
1230
|
const txHash = result.txHash ?? null;
|
|
1231
|
+
// MilestoneEscrowV3: the verdict is a RECOMMENDATION and moves nothing. Asked
|
|
1232
|
+
// first, so no arm below can read it as a payout or a rejection.
|
|
1233
|
+
const v3Recommendation = action === "V3_RECOMMENDATION";
|
|
1010
1234
|
let statusLine;
|
|
1011
|
-
if (
|
|
1235
|
+
if (v3Recommendation) {
|
|
1236
|
+
statusLine = v3RecommendationLine(result.submissionVerdict, confidence);
|
|
1237
|
+
} else if (superseded) {
|
|
1012
1238
|
// `settledAs` names a terminal row (e.g. EXPIRED) the verdict lost to, not a
|
|
1013
1239
|
// competing proof — "another proof won" would send the agent to resubmit.
|
|
1014
1240
|
const settledAs = result.settledAs;
|
|
@@ -1017,18 +1243,19 @@ async function handleVerify({ proofId }) {
|
|
|
1017
1243
|
: `ℹ️ SUPERSEDED — another proof on this milestone was decided first, so this verification did not change its state. Your evidence was recorded. Check the milestone status before resubmitting.`;
|
|
1018
1244
|
} else if (released) {
|
|
1019
1245
|
statusLine = alreadyReleased
|
|
1020
|
-
? `✅ COMPLETED —
|
|
1021
|
-
: `✅ COMPLETED (${confidence}% confidence) —
|
|
1246
|
+
? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
|
|
1247
|
+
: `✅ COMPLETED (${confidence}% confidence) — no money was held (verification-only contract)${txHash ? `, tx ${txHash}` : ""}.`;
|
|
1022
1248
|
} else if (complianceBlocked) {
|
|
1023
1249
|
statusLine = `⏸️ VERIFIED (${confidence}%) — payout on hold pending compliance review. No funds moved.`;
|
|
1024
|
-
} else if (
|
|
1025
|
-
statusLine =
|
|
1250
|
+
} else if (notCompletable) {
|
|
1251
|
+
statusLine = `⛔ NOT COMPLETED (${confidence}% confidence) — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
|
|
1026
1252
|
} else if (passed) {
|
|
1027
1253
|
statusLine = `✅ VERIFIED (${confidence}% confidence)`;
|
|
1028
1254
|
} else if (action === "VERIFIED") {
|
|
1029
|
-
// Approved,
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1255
|
+
// Approved, but no completion followed. Since Phase 6 every completion outcome
|
|
1256
|
+
// answers its own action, so the producer does not reach this today — it says so
|
|
1257
|
+
// rather than promising a retry that no longer exists.
|
|
1258
|
+
statusLine = `⚠️ VERIFIED (${confidence}% confidence) — the work was approved, but the milestone was NOT completed. No money moved. Check it with cascrow_get_contract; if it stays VERIFIED, contact support@cascrow.com.`;
|
|
1032
1259
|
} else if (panelUnavailable) {
|
|
1033
1260
|
statusLine = `⏳ PENDING_REVIEW — the verification panel did not answer (too few models responded). This is NOT a rejection and NOT a judgement of the work; a human decides. Resubmitting the same evidence will not help while the panel is down.`;
|
|
1034
1261
|
} else if (pendingReview) {
|
|
@@ -1048,6 +1275,11 @@ async function handleVerify({ proofId }) {
|
|
|
1048
1275
|
decision,
|
|
1049
1276
|
confidence,
|
|
1050
1277
|
action,
|
|
1278
|
+
v3Recommendation,
|
|
1279
|
+
// MilestoneEscrowV3: the claim-relevant fields, structured as well as rendered.
|
|
1280
|
+
...(v3Recommendation
|
|
1281
|
+
? { manifestHash: result.manifestHash ?? null, submissionVerdict: result.submissionVerdict ?? "PENDING", claimReady: result.claimReady === true }
|
|
1282
|
+
: {}),
|
|
1051
1283
|
passed,
|
|
1052
1284
|
pendingReview,
|
|
1053
1285
|
// Returned as well as rendered: a client that reads the structured fields rather
|
|
@@ -1060,7 +1292,7 @@ async function handleVerify({ proofId }) {
|
|
|
1060
1292
|
// Returned as well as rendered, as the hosted route does: with SUPERSEDED it says
|
|
1061
1293
|
// the row was CLOSED rather than won by another proof.
|
|
1062
1294
|
...(result.settledAs ? { settledAs: result.settledAs } : {}),
|
|
1063
|
-
|
|
1295
|
+
notCompletableWithoutEscrow: notCompletable,
|
|
1064
1296
|
reasoning,
|
|
1065
1297
|
totalModels: TOTAL_MODELS,
|
|
1066
1298
|
modelsResponded: votes.length,
|
|
@@ -1078,9 +1310,14 @@ async function handleGetContract({ contractId }) {
|
|
|
1078
1310
|
|
|
1079
1311
|
async function handleJoinContract({ inviteCode }) {
|
|
1080
1312
|
const data = await apiPost("/api/contracts/join", { inviteCode });
|
|
1313
|
+
// `activated` is the join's own activation result: a $0 contract is active now, a
|
|
1314
|
+
// money contract waits for its V3 offer. It said "AWAITING_ESCROW — ready to receive
|
|
1315
|
+
// proof once funded" for both, which sent a $0 builder waiting for money that never comes.
|
|
1081
1316
|
return {
|
|
1082
1317
|
contractId: data.contractId,
|
|
1083
|
-
|
|
1318
|
+
status: data.status ?? null,
|
|
1319
|
+
activated: typeof data.activated === "boolean" ? data.activated : null,
|
|
1320
|
+
message: joinedLine(data),
|
|
1084
1321
|
};
|
|
1085
1322
|
}
|
|
1086
1323
|
|
|
@@ -1119,9 +1356,8 @@ async function handleCheckInvites() {
|
|
|
1119
1356
|
// counterparty wrote about it: `name`, `skills` (PATCH /api/agent/settings,
|
|
1120
1357
|
// 20 x 50 chars, no charset restriction) and a milestone `summary`. These tools
|
|
1121
1358
|
// hand that straight into a DIFFERENT principal's model context while it is
|
|
1122
|
-
// choosing who to hire
|
|
1123
|
-
//
|
|
1124
|
-
// text says.
|
|
1359
|
+
// choosing who to hire and about to fund them. Unmarked, cascrow is the
|
|
1360
|
+
// transport for whatever the text says.
|
|
1125
1361
|
//
|
|
1126
1362
|
// Why it is copied here rather than imported: this package ships to npm with
|
|
1127
1363
|
// one dependency (@modelcontextprotocol/sdk) and cannot reach into the web
|
|
@@ -1266,16 +1502,13 @@ async function handleGetReputation({ walletAddress }) {
|
|
|
1266
1502
|
: data.milestones,
|
|
1267
1503
|
};
|
|
1268
1504
|
const { stats, milestones = [], skills = [] } = marked;
|
|
1269
|
-
const successPct = stats.successRate !== null
|
|
1270
|
-
? `${Math.round(stats.successRate * 100)}%`
|
|
1271
|
-
: "no data yet";
|
|
1272
1505
|
|
|
1273
1506
|
const milestoneLines = milestones.slice(0, 5).map((m) => {
|
|
1274
1507
|
const proofLinks = Object.entries(m.proofs)
|
|
1275
1508
|
.filter(([, hash]) => hash)
|
|
1276
1509
|
.map(([k, hash]) => `${k}: ${xrplTxUrl(hash)}`)
|
|
1277
1510
|
.join(", ");
|
|
1278
|
-
return ` - ${m.summary ?? "Completed milestone"} ($${m.amountUSD}
|
|
1511
|
+
return ` - ${m.summary ?? "Completed milestone"} ($${m.amountUSD})${proofLinks ? ` — ${proofLinks}` : ""}`;
|
|
1279
1512
|
}).join("\n");
|
|
1280
1513
|
|
|
1281
1514
|
// An empty `milestones` array has two different causes and the agent has to be
|
|
@@ -1285,6 +1518,25 @@ async function handleGetReputation({ walletAddress }) {
|
|
|
1285
1518
|
// "Completed: 12" next to no evidence and no reason — a refusal with no reader
|
|
1286
1519
|
// (F-72). Kept byte-identical to the in-repo server's copy; the two are a pair
|
|
1287
1520
|
// this repo already knows about (SEC-009).
|
|
1521
|
+
// `stats` is null when the builder has not consented to publishing their
|
|
1522
|
+
// figures (the server answers 404 when nothing at all is public). Said in words,
|
|
1523
|
+
// because "no figures" must not read as "no track record". Byte-identical to the
|
|
1524
|
+
// in-repo server's copy.
|
|
1525
|
+
if (!stats) {
|
|
1526
|
+
return {
|
|
1527
|
+
...marked,
|
|
1528
|
+
message: [
|
|
1529
|
+
`Agent reputation for ${walletAddress}:`,
|
|
1530
|
+
" Figures: not published — this builder has not agreed to publish their track-record figures. Only the milestones they published one by one are shown.",
|
|
1531
|
+
skills.length > 0 ? ` Skills: ${skills.join(", ")}` : null,
|
|
1532
|
+
milestones.length > 0 ? `\nPublished milestones:\n${milestoneLines}` : null,
|
|
1533
|
+
].filter(Boolean).join("\n"),
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const successPct = stats.successRate !== null
|
|
1537
|
+
? `${Math.round(stats.successRate * 100)}%`
|
|
1538
|
+
: "no data yet";
|
|
1539
|
+
|
|
1288
1540
|
const noPublishedNote = stats.milestonesCompleted > 0
|
|
1289
1541
|
? "\nPublished milestones: none — this builder has not made any milestone record public. The counts above cover every eligible milestone."
|
|
1290
1542
|
: null;
|
|
@@ -1294,7 +1546,7 @@ async function handleGetReputation({ walletAddress }) {
|
|
|
1294
1546
|
message: [
|
|
1295
1547
|
`Agent reputation for ${walletAddress}:`,
|
|
1296
1548
|
` Completed: ${stats.milestonesCompleted} milestones`,
|
|
1297
|
-
|
|
1549
|
+
releasedLine(stats),
|
|
1298
1550
|
` Success rate: ${successPct}`,
|
|
1299
1551
|
skills.length > 0 ? ` Skills: ${skills.join(", ")}` : null,
|
|
1300
1552
|
milestones.length > 0 ? `\nPublished milestones:\n${milestoneLines}` : noPublishedNote,
|
|
@@ -1302,6 +1554,17 @@ async function handleGetReputation({ walletAddress }) {
|
|
|
1302
1554
|
};
|
|
1303
1555
|
}
|
|
1304
1556
|
|
|
1557
|
+
/**
|
|
1558
|
+
* The value released to the builder (owner decision 18, 2026-09-26): `totalReleasedUsd`
|
|
1559
|
+
* over every escrow version, the XRP a MilestoneEscrowV3 escrow paid next to it.
|
|
1560
|
+
* `totalRlusdReleased` is deprecated — the retired v2 escrow only — and is the
|
|
1561
|
+
* fallback for a server that predates the new field. Same line as the hosted server.
|
|
1562
|
+
*/
|
|
1563
|
+
function releasedLine(stats) {
|
|
1564
|
+
const xrp = stats.totalXrpReleased && Number(stats.totalXrpReleased) > 0 ? ` (${stats.totalXrpReleased} XRP from MilestoneEscrowV3 escrows)` : "";
|
|
1565
|
+
return ` Released to this builder: $${stats.totalReleasedUsd ?? stats.totalRlusdReleased}${xrp}`;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1305
1568
|
async function handleDiscoverAgents({ skill, minRate, limit } = {}) {
|
|
1306
1569
|
const params = new URLSearchParams();
|
|
1307
1570
|
if (skill) params.set("skill", skill);
|
|
@@ -1334,32 +1597,88 @@ async function handleDiscoverAgents({ skill, minRate, limit } = {}) {
|
|
|
1334
1597
|
};
|
|
1335
1598
|
}
|
|
1336
1599
|
|
|
1337
|
-
async function handleSetDiscoverable({ discoverable, skills, callbackUrl } = {}) {
|
|
1600
|
+
async function handleSetDiscoverable({ discoverable, showNameOnCertificates, consentVersion, skills, callbackUrl } = {}) {
|
|
1338
1601
|
const body = {};
|
|
1339
|
-
if (discoverable
|
|
1340
|
-
if (
|
|
1341
|
-
if (
|
|
1602
|
+
if (discoverable !== undefined) body.discoverable = discoverable;
|
|
1603
|
+
if (showNameOnCertificates !== undefined) body.showNameOnCertificates = showNameOnCertificates;
|
|
1604
|
+
if (consentVersion !== undefined) body.consentVersion = consentVersion;
|
|
1605
|
+
if (skills !== undefined) body.skills = skills;
|
|
1606
|
+
if (callbackUrl !== undefined) body.callbackUrl = callbackUrl;
|
|
1342
1607
|
|
|
1343
|
-
|
|
1608
|
+
let data;
|
|
1609
|
+
try {
|
|
1610
|
+
data = await apiPatch("/api/agent/settings", body, { "x-cascrow-channel": "mcp" });
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
// Turning a name consent on without the account holder's agreement is refused
|
|
1613
|
+
// by the server, and its answer carries the words to show. They are passed on
|
|
1614
|
+
// as they come, never kept here, so this package cannot show an outdated text.
|
|
1615
|
+
if (!(err instanceof CascrowApiError) || err.code !== "CONSENT_REQUIRED") throw err;
|
|
1616
|
+
const consents = Array.isArray(err.body?.consents) ? err.body.consents : [];
|
|
1617
|
+
const texts = consents.map((c) => `${c.purpose}: ${c.text?.en ?? ""}`).join("\n\n");
|
|
1618
|
+
return {
|
|
1619
|
+
code: "CONSENT_REQUIRED",
|
|
1620
|
+
consentVersion: err.body?.consentVersion ?? null,
|
|
1621
|
+
consents,
|
|
1622
|
+
message:
|
|
1623
|
+
"Nothing was changed — neither the switch nor any other setting sent with it. Turning this on publishes the account holder's name. Show them the text below; only if they agree, " +
|
|
1624
|
+
`call cascrow_set_discoverable again with consentVersion "${err.body?.consentVersion ?? ""}".\n\n${texts}`,
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1344
1627
|
|
|
1345
1628
|
const parts = [];
|
|
1346
1629
|
if (data.discoverable !== undefined)
|
|
1347
1630
|
parts.push(`Discoverable: ${data.discoverable ? "✅ yes" : "❌ no"}`);
|
|
1631
|
+
if (data.showNameOnCertificates !== undefined)
|
|
1632
|
+
parts.push(`Name on certificate check pages: ${data.showNameOnCertificates ? "✅ yes" : "❌ no"}`);
|
|
1348
1633
|
if (data.skills?.length > 0)
|
|
1349
1634
|
parts.push(`Skills: ${data.skills.join(", ")}`);
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1635
|
+
// A settings change never relays the callback secret into the model's context
|
|
1636
|
+
// (owner decision, 2026-10-19) — only cascrow_rotate_callback_secret does, after
|
|
1637
|
+
// its warning. The secret the server created is dropped here, unread.
|
|
1638
|
+
const { callbackSecret, callbackSecretNotice, ...shown } = data;
|
|
1639
|
+
void callbackSecretNotice;
|
|
1640
|
+
if (shown.callbackUrl)
|
|
1641
|
+
parts.push(`Callback URL: ${shown.callbackUrl}`);
|
|
1642
|
+
if (shown.callbackUrl && callbackSecret)
|
|
1643
|
+
parts.push("Callbacks are signed with a new secret that is not shown here — call cascrow_rotate_callback_secret to obtain it.");
|
|
1644
|
+
if (shown.profileUrl)
|
|
1645
|
+
parts.push(`Public profile: ${BASE_URL}${shown.profileUrl}`);
|
|
1354
1646
|
|
|
1355
1647
|
return {
|
|
1356
|
-
...
|
|
1648
|
+
...shown,
|
|
1357
1649
|
message: parts.length > 0
|
|
1358
1650
|
? `Agent settings updated:\n${parts.map((p) => ` ${p}`).join("\n")}`
|
|
1359
1651
|
: "Settings saved.",
|
|
1360
1652
|
};
|
|
1361
1653
|
}
|
|
1362
1654
|
|
|
1655
|
+
// The same words as src/lib/callback-secret-text.ts ROTATE_WARNING (this package
|
|
1656
|
+
// cannot import it; callback-secret-doors.test.ts holds the two equal).
|
|
1657
|
+
const ROTATE_WARNING =
|
|
1658
|
+
"WARNING: this replaces the callback signing secret. The previous secret stops verifying immediately — " +
|
|
1659
|
+
"callbacks signed from now on use the new one, so update your receiver at once. The new secret is returned " +
|
|
1660
|
+
"in this tool result, which means it enters this conversation's context and transcript: store it only in the " +
|
|
1661
|
+
"receiver's configuration, do not repeat it, and rotate again if this conversation may be read by anyone else.";
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* The ONE MCP door to the callback signing secret. Two steps: without `confirm`
|
|
1665
|
+
* nothing changes and the answer is the warning; with `confirm: true` it rotates
|
|
1666
|
+
* (POST /api/agent/settings/callback-secret) and returns the secret.
|
|
1667
|
+
*/
|
|
1668
|
+
async function handleRotateCallbackSecret({ confirm } = {}) {
|
|
1669
|
+
if (confirm !== true) {
|
|
1670
|
+
return { rotated: false, warning: ROTATE_WARNING, message: `${ROTATE_WARNING}\n\nNothing was changed. Call cascrow_rotate_callback_secret again with confirm: true to rotate.` };
|
|
1671
|
+
}
|
|
1672
|
+
let data;
|
|
1673
|
+
try {
|
|
1674
|
+
data = await apiPost("/api/agent/settings/callback-secret", {});
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
if (!(err instanceof CascrowApiError) || err.code !== "NO_CALLBACK_URL") throw err;
|
|
1677
|
+
return { rotated: false, code: "NO_CALLBACK_URL", message: "No callback URL is set, so there is no secret to rotate. Set one with cascrow_set_discoverable { callbackUrl }, then call this tool again." };
|
|
1678
|
+
}
|
|
1679
|
+
return { rotated: true, callbackSecret: data.callbackSecret, warning: ROTATE_WARNING, message: `${ROTATE_WARNING}\n\n${data.message ?? ""}` };
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1363
1682
|
/**
|
|
1364
1683
|
* The funder's pending-verdict queue, and the verdict itself.
|
|
1365
1684
|
*
|
|
@@ -1371,7 +1690,9 @@ async function handleSetDiscoverable({ discoverable, skills, callbackUrl } = {})
|
|
|
1371
1690
|
*
|
|
1372
1691
|
* The queue exists because a push without a pull is not a channel: an agent that
|
|
1373
1692
|
* was down when `manual_review.required` fired has no way back to the decision,
|
|
1374
|
-
* and `cron/cancel-expired`
|
|
1693
|
+
* and `cron/cancel-expired` completes it when its review window closes — 48 h
|
|
1694
|
+
* under MANUAL_AUTO/AUTO, 14 days otherwise (a verification-only contract: no
|
|
1695
|
+
* money moves, and nobody decided it — `decidedBy: "sla"`).
|
|
1375
1696
|
* The verdict ships in the same change rather than later, because a list its
|
|
1376
1697
|
* recipient cannot act on is the shape this project has already been burned by
|
|
1377
1698
|
* (F-72). This server and the hosted one at `src/app/api/mcp/route.ts` carry both
|
|
@@ -1380,9 +1701,12 @@ async function handleSetDiscoverable({ discoverable, skills, callbackUrl } = {})
|
|
|
1380
1701
|
async function handleReviewQueue() {
|
|
1381
1702
|
const data = await apiGet("/api/agent/review-queue");
|
|
1382
1703
|
const contracts = data.contracts ?? [];
|
|
1704
|
+
// The V3 hint is said in BOTH branches: an empty v2 queue is exactly when an
|
|
1705
|
+
// agent stops reading, and a claim window keeps running (post-review of 6faf3fdc).
|
|
1706
|
+
const v3 = v3ClaimsHint(data.v3ClaimsPending);
|
|
1383
1707
|
|
|
1384
1708
|
if (contracts.length === 0) {
|
|
1385
|
-
return { contracts: [], total: 0, truncated: false, message:
|
|
1709
|
+
return { ...data, contracts: [], total: 0, truncated: false, message: `No milestones are waiting for your verdict.${v3}` };
|
|
1386
1710
|
}
|
|
1387
1711
|
|
|
1388
1712
|
// The titles arrive already marked as counterparty-written by the route
|
|
@@ -1395,16 +1719,130 @@ async function handleReviewQueue() {
|
|
|
1395
1719
|
: pending.length > 1
|
|
1396
1720
|
? ` — ${pending.length} milestones pending; name one with milestoneId`
|
|
1397
1721
|
: " — contract-level review (omit milestoneId)";
|
|
1398
|
-
return ` - ${c.contractId}
|
|
1722
|
+
return ` - ${c.contractId} ${c.projectTitle}${ms}`;
|
|
1399
1723
|
}).join("\n");
|
|
1400
1724
|
|
|
1401
1725
|
const more = data.truncated ? `\n\n(${data.total} in total — this list is capped.)` : "";
|
|
1402
1726
|
return {
|
|
1403
1727
|
...data,
|
|
1404
|
-
message: `${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\nDecide with cascrow_review_contract
|
|
1728
|
+
message: `${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\nDecide with cascrow_review_contract.${v3}`,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function v3ClaimsHint(n) {
|
|
1733
|
+
if (n === null) return "\n\nOpen MilestoneEscrowV3 claims could not be counted — check cascrow_v3_claims.";
|
|
1734
|
+
if (typeof n === "number" && n > 0) return `\n\n${n} MilestoneEscrowV3 claim(s) can still be objected to — list them with cascrow_v3_claims.`;
|
|
1735
|
+
return "";
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* The payer's open MilestoneEscrowV3 claims — `GET /api/agent/v3-claims`, the
|
|
1740
|
+
* same owner the review-queue count asks (design Rev. 8 final C). Pull only: the
|
|
1741
|
+
* action is the payer's own `object()` on chain (rule I9). The hosted twin is
|
|
1742
|
+
* `handleV3Claims` in `src/app/api/mcp/route.ts`.
|
|
1743
|
+
*/
|
|
1744
|
+
async function handleV3Claims() {
|
|
1745
|
+
const data = await apiGet("/api/agent/v3-claims");
|
|
1746
|
+
const claims = data.claims ?? [];
|
|
1747
|
+
if (claims.length === 0) return { ...data, claims: [], message: "No MilestoneEscrowV3 claim is waiting on you." };
|
|
1748
|
+
const lines = claims
|
|
1749
|
+
.map((c) => ` - escrow ${c.escrowId} on ${c.chainId}/${c.escrowContract} — ${c.milestoneTitle}, claim ${c.claimIndex}` +
|
|
1750
|
+
`${c.secondFunding ? " (SECOND funding)" : ""}${c.held ? " (held by the indexer — verify on chain)" : ""}: ` +
|
|
1751
|
+
(c.objectable === null ? "claim state unknown here — read getEscrow on chain" : c.objectable ? `object() before ${c.objectionDeadline}` : `window closed at ${c.objectionDeadline}`))
|
|
1752
|
+
.join("\n");
|
|
1753
|
+
const more = data.truncated ? "\n\n(This list is capped; more claims exist.)" : "";
|
|
1754
|
+
return { ...data, message: `${claims.length} claim(s):\n${lines}${more}\n\ncascrow cannot object for you — your own wallet calls object(escrowId) on the escrow contract.` };
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// ── MilestoneEscrowV3, remote half (design Rev. 10). cascrow prepares and reads; the
|
|
1758
|
+
// party signs and sends with its own key — nothing here signs, sends or relays. The
|
|
1759
|
+
// hosted twins are the handleV3* functions in src/app/api/mcp/route.ts.
|
|
1760
|
+
async function handleV3Offer(args) {
|
|
1761
|
+
const data = await apiPost("/api/escrow/v3/offer", args);
|
|
1762
|
+
return { ...data, message: "Offer frozen and sent to the builder. They accept with their own key; you then fund with one transaction from your machine: npx cascrow-signer@0.2.0 fund --milestone " + args.milestoneId + "." };
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Whether the offer an acceptance would sign names no arbiter — read from the
|
|
1767
|
+
* typed data that is signed (the zero address), not from the convenience field.
|
|
1768
|
+
* The same function in the hosted server (src/app/api/mcp/route.ts).
|
|
1769
|
+
*/
|
|
1770
|
+
function noArbiter(data) {
|
|
1771
|
+
const a = data?.typedData?.message?.arbiter ?? data?.arbiter;
|
|
1772
|
+
return typeof a !== "string" || /^0x0{40}$/i.test(a);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
async function handleV3AcceptanceGet({ milestoneId }) {
|
|
1776
|
+
const data = await apiGet(`/api/escrow/v3/acceptance?milestoneId=${encodeURIComponent(milestoneId)}`);
|
|
1777
|
+
return {
|
|
1778
|
+
...data,
|
|
1779
|
+
message:
|
|
1780
|
+
"These are the payer's frozen terms. Check them yourself — your signature binds you until acceptBy, and nothing in cascrow revokes it. " +
|
|
1781
|
+
"npx cascrow-signer@0.2.0 accept --milestone " + milestoneId + " checks them against its own pinned chain and contract and signs on your machine." +
|
|
1782
|
+
// S-05: the free veto, said with the terms it applies to (owner: V3_CLIENT_TEXT).
|
|
1783
|
+
(noArbiter(data) ? ` ${V3_CLIENT_TEXT.noArbiterVeto}` : ""),
|
|
1405
1784
|
};
|
|
1406
1785
|
}
|
|
1407
1786
|
|
|
1787
|
+
async function handleV3AcceptanceSubmit({ offerId, signature }) {
|
|
1788
|
+
const data = await apiPost("/api/escrow/v3/acceptance", { offerId, signature });
|
|
1789
|
+
return { ...data, message: "Acceptance recorded. The payer can now fund these exact terms until acceptBy." };
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async function handleV3Prepare({ milestoneId, action, proofHash, reasonHash, newDeadline }) {
|
|
1793
|
+
const q = new URLSearchParams({ milestoneId, action });
|
|
1794
|
+
if (proofHash) q.set("proofHash", proofHash);
|
|
1795
|
+
if (reasonHash) q.set("reasonHash", reasonHash);
|
|
1796
|
+
if (newDeadline) q.set("newDeadline", String(newDeadline));
|
|
1797
|
+
const data = await apiGet(`/api/escrow/v3/prepare?${q.toString()}`);
|
|
1798
|
+
return {
|
|
1799
|
+
...data,
|
|
1800
|
+
message:
|
|
1801
|
+
"UNSIGNED transaction — cascrow does not sign, send or relay it. Before signing: rebuild data from the contract ABI, and check to and chainId against your OWN configuration, not this answer. " +
|
|
1802
|
+
"Then broadcast it yourself and report the hash with cascrow_v3_report_tx. npx cascrow-signer@0.2.0 does all of this locally.",
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
async function handleV3ReportTx({ milestoneId, txHash }) {
|
|
1807
|
+
// Refused BEFORE anything is sent: a signed transaction pasted here would be
|
|
1808
|
+
// cascrow receiving a transaction to relay, which it never does (rule I9).
|
|
1809
|
+
if (typeof txHash !== "string" || !TX_HASH_RE.test(txHash)) throw new CascrowApiError(V3_CLIENT_TEXT.reportTxOnlyHash, { status: 0, code: "NOT_A_TX_HASH" });
|
|
1810
|
+
const data = await apiPost("/api/escrow/v3/sync", { milestoneId, txHash });
|
|
1811
|
+
return { ...data, message: "Reported. cascrow re-reads the chain for this milestone; the transaction itself is only what the chain says it is." };
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
async function handleV3Attestation({ escrowId, claimIndex }) {
|
|
1815
|
+
const q = new URLSearchParams({ escrowId, claimIndex: String(claimIndex) });
|
|
1816
|
+
const data = await apiGet(`/api/escrow/v3/attestation?${q.toString()}`);
|
|
1817
|
+
return { ...data, message: "A non-binding AI recommendation, signed by cascrow. It moves no money: only the payer's release, an unobjected claim running out its window, or the arbiter pays." };
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* The two human-review doors — the same answers as the hosted server's
|
|
1822
|
+
* (src/app/api/mcp/route.ts). An open request is not an error for the agent: it has
|
|
1823
|
+
* nothing to do but wait, so ALREADY_OPEN comes back as a result with the deadline.
|
|
1824
|
+
*/
|
|
1825
|
+
async function handleRequestHumanReview({ contractId, milestoneId, reason }) {
|
|
1826
|
+
try {
|
|
1827
|
+
const data = await apiPost("/api/human-review", { contractId, ...(milestoneId ? { milestoneId } : {}), ...(reason ? { reason } : {}) });
|
|
1828
|
+
return { ...data, message: `Human review requested (request ${data.requestId}). A person at cascrow decides by ${data.dueAt}; both parties receive the decision with its reasons. Read it with cascrow_human_reviews { contractId: "${contractId}" }.` };
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
if (err instanceof CascrowApiError && err.code === "HUMAN_REVIEW_ALREADY_OPEN") {
|
|
1831
|
+
const dueAt = err.body && typeof err.body.dueAt === "string" ? err.body.dueAt : null;
|
|
1832
|
+
return { status: "ALREADY_OPEN", dueAt, message: `A human review of this milestone is already open${dueAt ? `; a person at cascrow decides by ${dueAt}` : ""}. There is no need to ask again.` };
|
|
1833
|
+
}
|
|
1834
|
+
throw err;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
async function handleHumanReviews({ contractId }) {
|
|
1839
|
+
const data = await apiGet(`/api/human-review?contractId=${encodeURIComponent(contractId)}`);
|
|
1840
|
+
const requests = Array.isArray(data.requests) ? data.requests : [];
|
|
1841
|
+
if (requests.length === 0) return { requests: [], message: "No human-review requests on this contract." };
|
|
1842
|
+
const lines = requests.map((r) => ` - ${r.id} [${r.status}]${r.source === "REVIEW_LAPSED" ? " (review window lapsed)" : ""} — ${r.outcome ? `outcome ${r.outcome}` : `decision due by ${r.dueAt}`}`).join("\n");
|
|
1843
|
+
return { ...data, message: `${requests.length} human-review request(s):\n${lines}` };
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1408
1846
|
async function handleReviewContract({ contractId, decision, milestoneId }) {
|
|
1409
1847
|
const data = await apiPost(
|
|
1410
1848
|
"/api/contracts/review",
|
|
@@ -1412,10 +1850,9 @@ async function handleReviewContract({ contractId, decision, milestoneId }) {
|
|
|
1412
1850
|
);
|
|
1413
1851
|
const what =
|
|
1414
1852
|
decision === "APPROVE"
|
|
1415
|
-
? "approved
|
|
1853
|
+
? "approved and completed (a verification-only contract: no money moves)"
|
|
1416
1854
|
: "rejected — the milestone is back with the builder, who can resubmit";
|
|
1417
|
-
|
|
1418
|
-
return { ...data, message: `Milestone ${what}.${tx}` };
|
|
1855
|
+
return { ...data, message: `Milestone ${what}.` };
|
|
1419
1856
|
}
|
|
1420
1857
|
|
|
1421
1858
|
async function handleListMyContracts({ role } = {}) {
|
|
@@ -1457,9 +1894,9 @@ async function handleListMyContracts({ role } = {}) {
|
|
|
1457
1894
|
*
|
|
1458
1895
|
* `decision` answers a question about the PROOF. What an agent needs is the state of
|
|
1459
1896
|
* the MILESTONE, and the two come apart three ways that all used to read as plain
|
|
1460
|
-
* "APPROVED": the
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1897
|
+
* "APPROVED": the milestone is already done (COMPLETED), it carries an amount nothing can
|
|
1898
|
+
* complete (`notCompletableWithoutEscrow`), or this proof lost to another one and the
|
|
1899
|
+
* milestone is in someone else's terminal state.
|
|
1463
1900
|
*/
|
|
1464
1901
|
function proofOutcomeLine(s) {
|
|
1465
1902
|
// FIRST, before `decision` is read at all: a milestone that is already terminal
|
|
@@ -1468,17 +1905,22 @@ function proofOutcomeLine(s) {
|
|
|
1468
1905
|
// here used to print "✅ APPROVED" for money that may be back with the funder.
|
|
1469
1906
|
const closed = closedMilestoneStatus(s);
|
|
1470
1907
|
if (closed && closed !== "COMPLETED") return closedOutcomeLine(s, closed);
|
|
1908
|
+
// Before any verdict is read: an approval cannot complete this milestone (it carries
|
|
1909
|
+
// an amount without a MilestoneEscrowV3 escrow). A YES here is not "done" — the
|
|
1910
|
+
// verdict doors answered 409 NOT_COMPLETABLE_WITHOUT_ESCROW and a case is open.
|
|
1911
|
+
if (s.notCompletableWithoutEscrow === true) return `⛔ NOT COMPLETABLE — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
|
|
1471
1912
|
// SECOND: the milestone is approved (VERIFIED) or paid (COMPLETED), and not on a YES
|
|
1472
1913
|
// from this proof. Its `decision` is then NO or absent, and printing it next to
|
|
1473
1914
|
// "COMPLETED — funds released" or "APPROVED — payout scheduled" read as THIS proof
|
|
1474
1915
|
// having been approved.
|
|
1475
1916
|
if (approvedWithoutThisVerdict(s)) return approvedWithoutThisVerdictLine(s);
|
|
1917
|
+
// MilestoneEscrowV3, not yet paid: a RECOMMENDATION that moves nothing, judged by
|
|
1918
|
+
// the SUBMISSION's verdict — never "✅ APPROVED" off this file's `decision`.
|
|
1919
|
+
if (s.escrowVersion === 3 && s.milestoneStatus !== "COMPLETED") return v3RecommendationLine(s.submissionVerdict, s.confidence);
|
|
1476
1920
|
const head =
|
|
1477
1921
|
s.milestoneStatus === "COMPLETED" || closed === "COMPLETED"
|
|
1478
|
-
?
|
|
1479
|
-
: s.
|
|
1480
|
-
? `⏸️ APPROVED — payout scheduled for ${s.releaseHoldUntil}, no funds have moved yet`
|
|
1481
|
-
: s.decision === "YES"
|
|
1922
|
+
? `✅ COMPLETED — ${s.escrowVersion === 3 ? "paid out on chain from its MilestoneEscrowV3 escrow" : "no money was held (verification-only contract)"}`
|
|
1923
|
+
: s.decision === "YES"
|
|
1482
1924
|
? "✅ APPROVED"
|
|
1483
1925
|
: "❌ REJECTED";
|
|
1484
1926
|
return `${head}${s.confidence != null ? ` (confidence: ${s.confidence}%)` : ""}`;
|
|
@@ -1515,13 +1957,25 @@ function terminalStory(reason, row) {
|
|
|
1515
1957
|
case "CANCELLED_AFTER_APPROVAL":
|
|
1516
1958
|
return "An earlier approval lost the race to the funder's on-chain cancel; the escrow was refunded to the funder.";
|
|
1517
1959
|
case "DEADLINE_EXPIRED":
|
|
1518
|
-
return "The deadline passed
|
|
1960
|
+
return "The deadline passed before the milestone was completed; any escrow it held went back to the funder (refunding the funder is the funder's own on-chain call for a MilestoneEscrowV3 escrow).";
|
|
1519
1961
|
case "RENEGOTIATION_REJECTED":
|
|
1520
|
-
return "A requested extension was rejected and the
|
|
1962
|
+
return "A requested extension was rejected and the milestone was closed; any escrow it held went back to the funder.";
|
|
1521
1963
|
case "NO_FUNDED_ESCROW":
|
|
1522
1964
|
return "No funded escrow existed on-chain for this milestone.";
|
|
1523
1965
|
case "CONTRACT_DECLINED":
|
|
1524
1966
|
return "The contract was declined or expired before anything was escrowed.";
|
|
1967
|
+
case "ARBITER_REFUNDED":
|
|
1968
|
+
return "The named arbiter decided for the funder, and the escrow was refunded to the funder.";
|
|
1969
|
+
case "REFUNDED_AFTER_OBJECTION_AI_NO":
|
|
1970
|
+
return "The funder objected to the builder's claim, the panel had not approved it, and the escrow was refunded to the funder.";
|
|
1971
|
+
case "REFUNDED_AFTER_OBJECTION_DISPUTED":
|
|
1972
|
+
return "The funder objected to a claim the panel had approved, and the escrow was refunded to the funder — recorded as a dispute, not as the builder's failure.";
|
|
1973
|
+
case "RETURNED_BY_PAYEE":
|
|
1974
|
+
return "The builder returned the escrow to the funder.";
|
|
1975
|
+
case "RETURNED_BEFORE_WORK":
|
|
1976
|
+
return "The builder returned the escrow to the funder before any work was claimed.";
|
|
1977
|
+
case "ACCEPTANCE_LAPSED":
|
|
1978
|
+
return "The builder accepted the escrow terms, but the funder did not fund them before the acceptance expired; the milestone ended with no money escrowed, and it does not count against the builder.";
|
|
1525
1979
|
default:
|
|
1526
1980
|
return `The reason was not recorded — check the ${row} before assuming where the funds are.`;
|
|
1527
1981
|
}
|
|
@@ -1608,7 +2062,7 @@ function approvedWithoutThisVerdict(s) {
|
|
|
1608
2062
|
|
|
1609
2063
|
/** Final: no later poll can change what this proof status says. */
|
|
1610
2064
|
function nothingLeftToDecide(s) {
|
|
1611
|
-
return closedMilestoneStatus(s) !== null || approvedWithoutThisVerdict(s);
|
|
2065
|
+
return closedMilestoneStatus(s) !== null || s.notCompletableWithoutEscrow === true || approvedWithoutThisVerdict(s);
|
|
1612
2066
|
}
|
|
1613
2067
|
|
|
1614
2068
|
/** The outcome line for `approvedWithoutThisVerdict`. It never says THIS proof was approved. */
|
|
@@ -1617,10 +2071,8 @@ function approvedWithoutThisVerdictLine(s) {
|
|
|
1617
2071
|
const ROW = row.toUpperCase();
|
|
1618
2072
|
const head =
|
|
1619
2073
|
closedMilestoneStatus(s) === "COMPLETED"
|
|
1620
|
-
? `✅ ${ROW} COMPLETED —
|
|
1621
|
-
:
|
|
1622
|
-
? `⏸️ ${ROW} VERIFIED — payout scheduled for ${s.releaseHoldUntil}, no funds have moved yet (the funder can contest until then)`
|
|
1623
|
-
: `✅ ${ROW} VERIFIED — the payout is not confirmed yet`;
|
|
2074
|
+
? `✅ ${ROW} COMPLETED — ${s.escrowVersion === 3 ? "paid out on chain from its MilestoneEscrowV3 escrow" : "no money was held (verification-only contract)"}`
|
|
2075
|
+
: `✅ ${ROW} VERIFIED — approved, not completed yet`;
|
|
1624
2076
|
const verdict =
|
|
1625
2077
|
s.decision === "NO"
|
|
1626
2078
|
? `The panel did NOT approve this proof (NO${s.confidence != null ? `, confidence: ${s.confidence}%` : ""}).`
|
|
@@ -1645,7 +2097,9 @@ function approvedWithoutThisVerdictLine(s) {
|
|
|
1645
2097
|
* reading stays.
|
|
1646
2098
|
*/
|
|
1647
2099
|
function parkedForHuman(s) {
|
|
1648
|
-
|
|
2100
|
+
// Never a MilestoneEscrowV3 proof (post-review L2): no human review decides one — its
|
|
2101
|
+
// milestone status is the chain fold's, and a PENDING_REVIEW there is no queue.
|
|
2102
|
+
return s.escrowVersion !== 3 && Boolean(s.pending) && s.running !== true && s.milestoneStatus === "PENDING_REVIEW";
|
|
1649
2103
|
}
|
|
1650
2104
|
|
|
1651
2105
|
const PARKED_MESSAGE =
|
|
@@ -1694,8 +2148,8 @@ async function handleGetProofStatus({ proofId }) {
|
|
|
1694
2148
|
// SEC-058 / SEC-059, the polling door. `decision === "YES" ? APPROVED : REJECTED`
|
|
1695
2149
|
// answers a question about the PROOF; what an agent needs is the state of the
|
|
1696
2150
|
// MILESTONE, and those come apart in three ways that all read as plain "APPROVED":
|
|
1697
|
-
// the
|
|
1698
|
-
//
|
|
2151
|
+
// the milestone is already done (COMPLETED), it carries an amount nothing can
|
|
2152
|
+
// complete (notCompletableWithoutEscrow), or another proof owns the
|
|
1699
2153
|
// milestone and this verdict changed nothing (SUPERSEDED upstream, which leaves the
|
|
1700
2154
|
// milestone in someone else's terminal state).
|
|
1701
2155
|
const parts = [
|
|
@@ -1719,6 +2173,20 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
|
|
|
1719
2173
|
const proofId = data.proof_id ?? null;
|
|
1720
2174
|
const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
|
|
1721
2175
|
|
|
2176
|
+
// MilestoneEscrowV3: a recommendation, never a payout — asked before every verdict
|
|
2177
|
+
// branch below, none of which may read it as approved or rejected.
|
|
2178
|
+
if (data.action === "V3_RECOMMENDATION") {
|
|
2179
|
+
return {
|
|
2180
|
+
...markVerificationResult({ ...data, model_votes: votes }),
|
|
2181
|
+
verdict: "v3_recommendation",
|
|
2182
|
+
v3Recommendation: true,
|
|
2183
|
+
manifestHash: data.manifestHash ?? null,
|
|
2184
|
+
claimReady: data.claimReady ?? null,
|
|
2185
|
+
submissionVerdict: data.submissionVerdict ?? "PENDING",
|
|
2186
|
+
message: v3RecommendationLine(data.submissionVerdict, data.confidence ?? null),
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1722
2190
|
// SEC-055, first divergence. `/api/mcp/submit` answers `{ ok: true, decided:
|
|
1723
2191
|
// "elsewhere", … }` at status **200 with no `verdict` key** when the milestone was
|
|
1724
2192
|
// settled by a concurrent request — the investor's review, or a second submission.
|
|
@@ -1740,7 +2208,6 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
|
|
|
1740
2208
|
...markVerificationResult({ model_votes: [] }),
|
|
1741
2209
|
on_chain_url: null,
|
|
1742
2210
|
proof_id: proofId,
|
|
1743
|
-
release_hold_until: null,
|
|
1744
2211
|
signed_at: null,
|
|
1745
2212
|
message: settledAs
|
|
1746
2213
|
? `ℹ️ This milestone was closed (${settledAs}) before this submission could settle it, so this submission changed nothing and moved no funds itself. ` +
|
|
@@ -1768,7 +2235,6 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
|
|
|
1768
2235
|
confidence: data.confidence ?? null,
|
|
1769
2236
|
on_chain_url: null,
|
|
1770
2237
|
proof_id: proofId,
|
|
1771
|
-
release_hold_until: null,
|
|
1772
2238
|
message:
|
|
1773
2239
|
`⏳ PENDING_REVIEW — the verification panel could not reach a verdict on this submission ` +
|
|
1774
2240
|
`(too few models answered). This is NOT a rejection and NOT a judgement of your work: ` +
|
|
@@ -1779,17 +2245,13 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
|
|
|
1779
2245
|
}
|
|
1780
2246
|
|
|
1781
2247
|
const verdict = data.verdict ?? "unknown";
|
|
1782
|
-
//
|
|
1783
|
-
//
|
|
1784
|
-
//
|
|
1785
|
-
// finds out by polling balances — or not at all.
|
|
1786
|
-
const releaseHoldUntil = data.release_hold_until ?? null;
|
|
2248
|
+
// An approved milestone that cannot complete is not a 200: the route answers 409
|
|
2249
|
+
// NOT_COMPLETABLE_WITHOUT_ESCROW, which `apiPost` throws and the error formatter
|
|
2250
|
+
// renders with its hint (`codeHints`).
|
|
1787
2251
|
|
|
1788
2252
|
let statusLine;
|
|
1789
2253
|
if (verdict === "approved") {
|
|
1790
|
-
statusLine =
|
|
1791
|
-
? `⏸️ APPROVED (${confidence}% confidence) — payout scheduled for ${releaseHoldUntil}. NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move.`
|
|
1792
|
-
: `✅ APPROVED (${confidence}% confidence)${onChainUrl ? ` — proof: ${onChainUrl}` : ""}`;
|
|
2254
|
+
statusLine = `✅ APPROVED (${confidence}% confidence)${onChainUrl ? ` — proof: ${onChainUrl}` : ""}`;
|
|
1793
2255
|
} else if (verdict === "pending_review") {
|
|
1794
2256
|
statusLine = `⏳ PENDING_REVIEW (${confidence}% confidence) — manual review triggered.`;
|
|
1795
2257
|
} else {
|
|
@@ -1808,7 +2270,6 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
|
|
|
1808
2270
|
...markVerificationResult({ reasoning: data.reasoning ?? "", model_votes: votes }),
|
|
1809
2271
|
on_chain_url: onChainUrl,
|
|
1810
2272
|
proof_id: proofId,
|
|
1811
|
-
release_hold_until: releaseHoldUntil,
|
|
1812
2273
|
signed_at: data.signed_at ?? null,
|
|
1813
2274
|
message: `${statusLine}${voteSummary ? `\n\n${voteSummary}` : ""}${data.reasoning ? `\n\n${excerptMarked(data.reasoning, 400)}` : ""}`,
|
|
1814
2275
|
};
|
|
@@ -2018,7 +2479,7 @@ async function handleWatchVerification({ milestoneId, content, filename, proofId
|
|
|
2018
2479
|
// ─── MCP Server ───────────────────────────────────────────────────────────────
|
|
2019
2480
|
|
|
2020
2481
|
const server = new Server(
|
|
2021
|
-
{ name: "cascrow", version: "1.
|
|
2482
|
+
{ name: "cascrow", version: "3.1.0" },
|
|
2022
2483
|
{ capabilities: { tools: {} } }
|
|
2023
2484
|
);
|
|
2024
2485
|
|
|
@@ -2029,12 +2490,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2029
2490
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2030
2491
|
const { name, arguments: args } = request.params;
|
|
2031
2492
|
|
|
2493
|
+
// Removed in 2.0.0 (cascrow Phase 5): the tool sent the agent's private key to
|
|
2494
|
+
// the server. A host with a cached tool list may still call it. Answered before
|
|
2495
|
+
// `args` is touched and in anonymous mode too — the arguments may hold a key,
|
|
2496
|
+
// and nothing here logs, echoes or forwards them.
|
|
2497
|
+
if (REMOVED_CUSTODY_TOOLS.includes(name)) {
|
|
2498
|
+
return { content: [{ type: "text", text: CUSTODY_TOOL_REMOVED }], isError: true };
|
|
2499
|
+
}
|
|
2500
|
+
// Removed in 3.0.0 (cascrow Phase 6, owner decision 16): a $0 contract activates by
|
|
2501
|
+
// itself, and a milestone with an amount is a MilestoneEscrowV3 escrow its payer
|
|
2502
|
+
// funds from their own machine. A cached tool list still calls the old name.
|
|
2503
|
+
if (name === "cascrow_fund_milestone") {
|
|
2504
|
+
return { content: [{ type: "text", text: `Error: ${V3_CLIENT_TEXT.fundRemoved} [TOOL_REMOVED]` }], isError: true };
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2032
2507
|
if (ANONYMOUS_MODE && name !== "cascrow_verify_work") {
|
|
2033
2508
|
return {
|
|
2034
2509
|
content: [{
|
|
2035
2510
|
type: "text",
|
|
2036
2511
|
text: `Error: "${name}" requires a Cascrow account. Register free at https://cascrow.com/register ` +
|
|
2037
|
-
`(or POST /api/agent/register), then set CASCROW_API_KEY to unlock all
|
|
2512
|
+
`(or POST /api/agent/register), then set CASCROW_API_KEY to unlock all ${TOOLS.length} tools. ` +
|
|
2038
2513
|
`Only cascrow_verify_work works anonymously (3 free verifications/day).`,
|
|
2039
2514
|
}],
|
|
2040
2515
|
isError: true,
|
|
@@ -2045,8 +2520,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2045
2520
|
let result;
|
|
2046
2521
|
switch (name) {
|
|
2047
2522
|
case "cascrow_create_contract": result = await handleCreateContract(args); break;
|
|
2048
|
-
case "cascrow_fund_milestone": result = await handleFundMilestone(args); break;
|
|
2049
|
-
case "cascrow_escrow_fund": result = await handleEscrowFund(args); break;
|
|
2050
2523
|
case "cascrow_submit_proof": result = await handleSubmitProof(args); break;
|
|
2051
2524
|
case "cascrow_verify": result = await handleVerify(args); break;
|
|
2052
2525
|
case "cascrow_get_contract": result = await handleGetContract(args); break;
|
|
@@ -2057,9 +2530,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2057
2530
|
case "cascrow_get_reputation": result = await handleGetReputation(args); break;
|
|
2058
2531
|
case "cascrow_discover_agents": result = await handleDiscoverAgents(args); break;
|
|
2059
2532
|
case "cascrow_set_discoverable": result = await handleSetDiscoverable(args); break;
|
|
2533
|
+
case "cascrow_rotate_callback_secret": result = await handleRotateCallbackSecret(args); break;
|
|
2060
2534
|
case "cascrow_list_my_contracts": result = await handleListMyContracts(args); break;
|
|
2061
2535
|
case "cascrow_review_queue": result = await handleReviewQueue(); break;
|
|
2536
|
+
case "cascrow_v3_claims": result = await handleV3Claims(); break;
|
|
2537
|
+
case "cascrow_v3_offer": result = await handleV3Offer(args); break;
|
|
2538
|
+
case "cascrow_v3_acceptance_get": result = await handleV3AcceptanceGet(args); break;
|
|
2539
|
+
case "cascrow_v3_acceptance_submit": result = await handleV3AcceptanceSubmit(args); break;
|
|
2540
|
+
case "cascrow_v3_prepare": result = await handleV3Prepare(args); break;
|
|
2541
|
+
case "cascrow_v3_report_tx": result = await handleV3ReportTx(args); break;
|
|
2542
|
+
case "cascrow_v3_attestation": result = await handleV3Attestation(args); break;
|
|
2062
2543
|
case "cascrow_review_contract": result = await handleReviewContract(args); break;
|
|
2544
|
+
case "cascrow_request_human_review": result = await handleRequestHumanReview(args); break;
|
|
2545
|
+
case "cascrow_human_reviews": result = await handleHumanReviews(args); break;
|
|
2063
2546
|
case "cascrow_get_proof_status": result = await handleGetProofStatus(args); break;
|
|
2064
2547
|
case "cascrow_mcp_submit": result = await handleMcpSubmit(args); break;
|
|
2065
2548
|
case "cascrow_verify_work": result = await handleVerifyWork(args); break;
|
|
@@ -2072,8 +2555,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2072
2555
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2073
2556
|
};
|
|
2074
2557
|
} catch (err) {
|
|
2558
|
+
// A V3 refusal says who can act and how (F-72) — the body's actionable fields (G5),
|
|
2559
|
+
// then the owner's hint, role-aware on the body (H9); the owner's words, copied above.
|
|
2560
|
+
const detail = err instanceof CascrowApiError ? v3RefusalDetail(err.body) : "";
|
|
2561
|
+
const hint = err instanceof CascrowApiError ? v3HintFor(err.code, err.body) : null;
|
|
2075
2562
|
return {
|
|
2076
|
-
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
2563
|
+
content: [{ type: "text", text: `Error: ${err.message}${detail && !err.message.includes(detail) ? ` — ${detail}` : ""}${hint && !err.message.includes(hint) ? `\n\n${hint}` : ""}` }],
|
|
2077
2564
|
isError: true,
|
|
2078
2565
|
};
|
|
2079
2566
|
}
|