cascrow-cli 1.4.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/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "cascrow-cli",
3
- "version": "1.4.0",
4
- "description": "CLI for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and milestone escrow on the XRP Ledger",
3
+ "version": "3.1.0",
4
+ "description": "CLI for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and non-custodial milestone escrow (MilestoneEscrowV3, native XRP)",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "README.md",
10
+ "CHANGELOG.md",
11
+ "LICENSE"
12
+ ],
7
13
  "bin": {
8
14
  "cascrow": "index.js"
9
15
  },
package/builder-agent.js DELETED
@@ -1,430 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Cascrow Builder Agent
4
- * Polls for pending contract invites, then automatically:
5
- * joins → funds → submits proof → triggers AI verification
6
- */
7
-
8
- const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
9
- const API_KEY = process.env.CASCROW_API_KEY ?? "";
10
- const AGENT_EVM_WALLET = process.env.AGENT_EVM_WALLET ?? "";
11
- const POLL_INTERVAL_MS = 3000;
12
-
13
- if (!API_KEY) {
14
- console.error("ERROR: CASCROW_API_KEY is not set");
15
- process.exit(1);
16
- }
17
-
18
- // Every call reads the body as text and parses it in a try (an HTML 502 used to
19
- // crash the agent as "Unexpected token '<'"), carries a deadline over the body
20
- // read, and throws an Error with `status` and `code` (T11-A4). Upload's deadline
21
- // mirrors UPLOAD_CLIENT_ABORT_MS in src/lib/latency-budget.ts.
22
- const UPLOAD_CLIENT_ABORT_MS = 120_000;
23
- const REQUEST_CLIENT_ABORT_MS = 300_000;
24
-
25
- async function readJsonSafe(res) {
26
- const text = await res.text();
27
- if (!text) return {};
28
- try {
29
- return JSON.parse(text);
30
- } catch {
31
- return { error: `HTTP ${res.status} (non-JSON response from server)`, code: "UPSTREAM_NON_JSON" };
32
- }
33
- }
34
-
35
- async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT_ABORT_MS } = {}) {
36
- const controller = new AbortController();
37
- const timer = setTimeout(() => controller.abort(), deadlineMs);
38
- let res;
39
- let data;
40
- try {
41
- res = await fetch(`${BASE_URL}${path}`, {
42
- method,
43
- headers: { "Authorization": `Bearer ${API_KEY}`, ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
44
- ...(json !== undefined ? { body: JSON.stringify(json) } : form !== undefined ? { body: form } : {}),
45
- signal: controller.signal,
46
- });
47
- data = await readJsonSafe(res);
48
- } catch (e) {
49
- if (e && e.name === "AbortError") {
50
- throw Object.assign(new Error(`No answer from ${path} within ${Math.round(deadlineMs / 1000)} s [CLIENT_TIMEOUT]`), { status: 0, code: "CLIENT_TIMEOUT" });
51
- }
52
- throw e;
53
- } finally {
54
- clearTimeout(timer);
55
- }
56
- if (!res.ok || data.code === "UPSTREAM_NON_JSON") {
57
- const code = typeof data.code === "string" ? data.code : null;
58
- throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), { status: res.status, code });
59
- }
60
- return data;
61
- }
62
-
63
- async function apiPost(path, body) {
64
- return fetchJson("POST", path, { json: body });
65
- }
66
-
67
- async function apiGet(path) {
68
- return fetchJson("GET", path);
69
- }
70
-
71
- async function uploadProof(milestoneId, content) {
72
- const blob = new Blob([content], { type: "text/plain" });
73
- const form = new FormData();
74
- form.append("file", blob, "proof-report.txt");
75
- form.append("milestoneId", milestoneId);
76
- return fetchJson("POST", "/api/proof/upload", { form, deadlineMs: UPLOAD_CLIENT_ABORT_MS });
77
- }
78
-
79
- // SEC-073. The FIFTH client door of `/api/verify`, and until 2026-09-22 the only
80
- // one with no deadline at all: it was invisible to `latency-budget.test.ts`,
81
- // whose client list was three hand-typed files — and this file lives inside one
82
- // of the three packages already on that list, so the subject was PACKAGES where
83
- // the population is FILES (F-30).
84
- //
85
- // 235_000 — one step below the route's 240 s `maxDuration`, NOT "above the
86
- // worst case": the tail (pre-stream + a 15 s blob read + the 205 s ladder + the
87
- // bounded EVM retry) reaches ~239 s, so no value is both above it and below the
88
- // cap. See VERIFY_TAIL_WORST_CASE_MS in src/lib/latency-budget.ts. Cleared after
89
- // the READ LOOP rather than at the `fetch`: `fetch` resolves on response headers about 12 s in, and the whole
90
- // retry ladder arrives afterwards out of `res.body`.
91
- async function streamVerify(proofId) {
92
- const controller = new AbortController();
93
- const timeout = setTimeout(() => controller.abort(), 235_000);
94
- let res;
95
- try {
96
- res = await fetch(`${BASE_URL}/api/verify`, {
97
- method: "POST",
98
- headers: {
99
- "Authorization": `Bearer ${API_KEY}`,
100
- "Content-Type": "application/json",
101
- "Accept": "text/event-stream",
102
- },
103
- body: JSON.stringify({ proofId }),
104
- signal: controller.signal,
105
- });
106
- } catch (e) {
107
- clearTimeout(timeout);
108
- // SEC-073. Now that the deadline is live it can actually fire, and a raw
109
- // `DOMException: This operation was aborted` names no remedy — worse, per
110
- // the tail arithmetic in latency-budget.ts the abort can land AFTER
111
- // releaseMilestone(), so "it failed" may be false about the money.
112
- if (e && e.name === "AbortError") {
113
- throw new Error(
114
- "Verification is still running on the server — this client stopped waiting, it did not fail. " +
115
- `Poll GET /api/agent/proof-status/${proofId} for the verdict.`
116
- );
117
- }
118
- throw e;
119
- }
120
- if (!res.ok) {
121
- clearTimeout(timeout);
122
- const data = await readJsonSafe(res);
123
- // T01-8. Upload starts verification by itself, and this agent then starts it
124
- // again — so losing the claim to a RUNNING run is the normal case, not a failure.
125
- // The caller's polling path waits for that run; `alreadyRunning` sends it there.
126
- // Never read this 409's Retry-After as a wait: it is the ~1 h takeover cutoff.
127
- if (res.status === 409 && data.code === "VERIFICATION_IN_PROGRESS" && data.phase === "running") {
128
- throw Object.assign(new Error("A verification of this proof is already running (the one upload started)."), { alreadyRunning: true });
129
- }
130
- const code = typeof data.code === "string" ? data.code : null;
131
- throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), { status: res.status, code });
132
- }
133
- if (!res.body) {
134
- clearTimeout(timeout);
135
- throw new Error("Verification stream had no body");
136
- }
137
-
138
- const reader = res.body.getReader();
139
- const decoder = new TextDecoder();
140
- let buffer = "";
141
-
142
- try {
143
- while (true) {
144
- const { done, value } = await reader.read();
145
- if (done) break;
146
- buffer += decoder.decode(value, { stream: true });
147
- const lines = buffer.split("\n");
148
- buffer = lines.pop() ?? "";
149
- for (const line of lines) {
150
- if (!line.startsWith("data:")) continue;
151
- const raw = line.slice(5).trim();
152
- if (!raw) continue;
153
- try {
154
- const msg = JSON.parse(raw);
155
- if (msg.type === "model_vote") {
156
- const icon = msg.decision === "YES" ? "✅" : "❌";
157
- console.log(` ${icon} ${msg.model}: ${msg.decision} (${msg.confidence}%)`);
158
- } else if (msg.type === "complete") {
159
- return msg;
160
- } else if (msg.type === "error") {
161
- throw new Error(msg.message);
162
- }
163
- } catch (e) {
164
- if (e instanceof SyntaxError) continue;
165
- throw e;
166
- }
167
- }
168
- }
169
- } catch (e) {
170
- // The deadline can now fire DURING the read — that is the whole point of
171
- // clearing it below the loop rather than at the fetch. Same translation as
172
- // the fetch-side catch above.
173
- if (e && e.name === "AbortError") {
174
- throw new Error(
175
- "Verification is still running on the server — this client stopped waiting, it did not fail. " +
176
- `Poll GET /api/agent/proof-status/${proofId} for the verdict.`
177
- );
178
- }
179
- throw e;
180
- } finally {
181
- clearTimeout(timeout);
182
- reader.cancel().catch(() => {});
183
- }
184
- throw new Error("Stream ended without complete event");
185
- }
186
-
187
- async function handleInvite(invite) {
188
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
189
- console.log(`📨 New contract received!`);
190
- console.log(` Contract: ${invite.contractId}`);
191
- if (invite.message) console.log(` Task: ${invite.message}`);
192
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
193
-
194
- // 1. Join contract
195
- console.log("🔗 Joining contract...");
196
- const joined = await apiPost("/api/contracts/join", { inviteCode: invite.inviteCode });
197
- console.log(` ✓ Joined. Contract ID: ${joined.contractId}`);
198
-
199
- // 2. Wait for Requester to fund the milestone
200
- console.log("⏳ Waiting for milestone to be funded...");
201
- let fundedMilestoneId = null;
202
- for (let i = 0; i < 40; i++) {
203
- const contract = await apiGet(`/api/contracts/${joined.contractId}`);
204
- const funded = (contract.milestones ?? []).find((m) => m.status === "FUNDED");
205
- if (funded) {
206
- fundedMilestoneId = funded.id;
207
- console.log(` ✓ Milestone funded. ID: ${fundedMilestoneId}`);
208
- break;
209
- }
210
- await new Promise((r) => setTimeout(r, 3000));
211
- }
212
- if (!fundedMilestoneId) throw new Error("Timed out waiting for milestone to be funded");
213
-
214
- // 3. Do the work — generate proof based on the task message
215
- console.log("🤖 Performing task...");
216
- const task = invite.message ?? "Complete the assigned milestone";
217
- const bio = `Cascrow is the trust layer for the agent economy — an AI-powered escrow platform that locks RLUSD stablecoins on the XRPL EVM Sidechain and releases funds automatically when a five-model AI majority vote confirms a milestone has been completed.
218
-
219
- Where traditional escrow relies on human arbitrators and slow dispute resolution, Cascrow replaces that entirely with a parallel consensus engine: Claude, GPT-4o, Gemini, Mistral, and Cerebras evaluate every proof submission simultaneously. Three out of five approvals triggers an on-chain release — no intermediaries, no delays.
220
-
221
- Built for a world where autonomous agents transact on behalf of humans and each other, Cascrow provides three agent-native interfaces: a REST API, an MCP server, and a CLI — so any agent can create contracts, fund milestones, submit proof, and receive payment entirely programmatically.
222
-
223
- Launched at the XRPL Student Builder Residency, Spring 2026. Live at cascrow.com.`;
224
-
225
- const proof = `PROOF OF COMPLETION
226
- ═══════════════════════════════════════
227
-
228
- Task: ${task}
229
- Contract ID: ${invite.contractId}
230
- Milestone ID: ${fundedMilestoneId}
231
- Completed at: ${new Date().toISOString()}
232
-
233
- DELIVERABLE (149 words):
234
- ${bio}
235
-
236
- VERIFICATION CRITERIA MET:
237
- ✓ Professional third-person tone throughout
238
- ✓ 149 words — within the 140–160 word range
239
- ✓ Clearly explains what Cascrow does (AI-verified escrow)
240
- ✓ Highlights AI verification mechanism (5-model majority vote, named models)
241
- ✓ Addresses agent economy angle (autonomous agents, programmatic interfaces)
242
- ✓ No placeholder text
243
- ✓ Suitable for About page or press kit`;
244
-
245
- console.log(` ✓ Task complete.`);
246
-
247
- // 4. Submit proof
248
- console.log("📤 Submitting proof...");
249
- const proofData = await uploadProof(fundedMilestoneId, proof);
250
- console.log(` ✓ Proof submitted. ID: ${proofData.proofId}`);
251
-
252
- // 5. Trigger verification
253
- console.log("\n🔍 Triggering AI verification (5 models)...\n");
254
- let result;
255
- try {
256
- result = await streamVerify(proofData.proofId);
257
- } catch (streamErr) {
258
- // Stream disconnected before complete event (e.g. Vercel 150s timeout).
259
- // The verification is still running server-side — just poll for the result,
260
- // do NOT call streamVerify again (would trigger duplicate AI verification).
261
- // Two ways here, and the loop below serves both: the stream dropped mid-run, or
262
- // it never opened because upload's own run already held the claim (T01-8).
263
- if (streamErr && streamErr.alreadyRunning) {
264
- console.log(` ℹ️ Verification was already running (upload starts it) — waiting for its result...`);
265
- } else if (streamErr && (streamErr.status === 401 || streamErr.status === 403 || streamErr.status === 404)) {
266
- throw streamErr;
267
- } else {
268
- console.log(` ⚠️ Stream ended early, polling for result...`);
269
- }
270
- // SEC-076. This polled `contract.status`, and that column answers a different
271
- // question than the one asked here. It is a single slot with nine writers:
272
- // funding a SIBLING milestone writes `contractStatusAfterFunding(...)` over
273
- // it under `status: { notIn: ["COMPLETED","EXPIRED"] }`, so it fails both
274
- // ways. False negative: the pin is overwritten with FUNDED, no terminal value
275
- // ever appears, and this loop dies after five minutes on "Timed out waiting
276
- // for verification result" for a proof that was in fact decided. False
277
- // positive: the contract reads VERIFIED/COMPLETED because ANOTHER milestone
278
- // settled, and the builder is handed a verdict that is not their proof's -
279
- // `decision` below is derived from it.
280
- //
281
- // `GET /api/contracts/{id}` returns `milestones: [{ id, status, … }]`, and we
282
- // funded a specific one, so the honest read is that row. The contract-level
283
- // read is kept only for the legacy population that has no milestone rows at
284
- // all (proofs written via the contractId-only path).
285
- //
286
- // EXPIRED is terminal too. Since 2026-09-23 `/api/verify` no longer reopens a
287
- // milestone that went EXPIRED while its panel ran, so without it this loop polled a
288
- // closed row for five minutes and then threw "Timed out" — and it must never be
289
- // mapped to a YES: the milestone moved no funds through this verification.
290
- const TERMINAL_PROOF_STATES = ["VERIFIED", "COMPLETED", "REJECTED", "PENDING_REVIEW", "EXPIRED"];
291
- for (let i = 0; i < 60; i++) {
292
- await new Promise((r) => setTimeout(r, 5000));
293
- let contract;
294
- try {
295
- contract = await apiGet(`/api/contracts/${joined.contractId}`);
296
- } catch (e) {
297
- // 401/403/404 do not change by waiting; anything else is transient. One
298
- // dropped poll used to end the whole run.
299
- if (e && (e.status === 401 || e.status === 403 || e.status === 404)) throw e;
300
- continue;
301
- }
302
- const mine = (contract.milestones ?? []).find((m) => m.id === fundedMilestoneId);
303
- const observed = mine ? mine.status : (contract.milestones ?? []).length === 0 ? contract.status : null;
304
- if (observed && TERMINAL_PROOF_STATES.includes(observed)) {
305
- if (observed === "EXPIRED") {
306
- // No verdict is read on this path: the stream that carried it dropped, and
307
- // the row says only that it is closed.
308
- result = { decision: null, confidence: 0, action: "EXPIRED", milestoneClosed: true };
309
- console.log(` ✓ Got result via polling: ${observed}`);
310
- break;
311
- }
312
- const decision = ["VERIFIED", "COMPLETED"].includes(observed) ? "YES" : "NO";
313
- result = { decision, confidence: 0, action: observed };
314
- console.log(` ✓ Got result via polling: ${observed}`);
315
- break;
316
- }
317
- }
318
- if (!result) throw new Error("Timed out waiting for verification result");
319
- }
320
-
321
- // SEC-055/058/059. This re-implemented the three-tier rule from the PROOF's verdict
322
- // (`decision === "YES" && confidence >= 85`), so `COMPLETED`, `SUPERSEDED`, a
323
- // compliance block and a release hold all printed "✅ VERIFIED", and a panel outage
324
- // (`decision: "PENDING"`, confidence 0) printed "❌ NOT VERIFIED — 0% confidence" —
325
- // our own outage read back to the builder as a rejection of their work.
326
- //
327
- // `passed` now means FUNDS MOVED, and only `COMPLETED` says that. The polling
328
- // fallback above already derives `action` from the funded MILESTONE's status
329
- // (SEC-076; it read the contract row until 2026-09-22), so both paths reach this
330
- // with the same vocabulary.
331
- const released = result.action === "COMPLETED";
332
- const passed = released;
333
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
334
- if (result.milestoneClosed === true && result.action === "EXPIRED") {
335
- // `GET /api/contracts/{id}` does not return the milestone's `terminalReason`, so this
336
- // cannot say whether the funder was refunded or the builder was paid on-chain, and
337
- // does not guess.
338
- console.log(
339
- `⛔ CLOSED — the milestone was closed (EXPIRED) while the panel ran or after it decided, so this verification moved no funds. ` +
340
- `The reason is not available here — check the contract page before assuming where the funds are. Do not resubmit to it.`
341
- );
342
- } else if (result.action === "SUPERSEDED") {
343
- console.log(
344
- result.settledAs
345
- ? `ℹ️ SUPERSEDED — this milestone was closed (${result.settledAs}) before this verification could settle it; it changed nothing and moved no funds. Do not resubmit to it.`
346
- : `ℹ️ SUPERSEDED — another proof on this milestone was decided first; this changed nothing.`
347
- );
348
- } else if (released) {
349
- console.log(
350
- result.alreadyReleased === true
351
- ? `✅ COMPLETED — already released by another request. The funds are out; do not resubmit.`
352
- : `✅ COMPLETED — ${result.confidence}% confidence, funds released.`
353
- );
354
- } else if (result.complianceBlocked === true) {
355
- console.log(`⏸️ VERIFIED — payout on hold pending compliance review. No funds moved.`);
356
- } else if (result.releaseHoldUntil) {
357
- console.log(`⏸️ VERIFIED — approved, payout scheduled for ${result.releaseHoldUntil}. NO FUNDS HAVE MOVED YET.`);
358
- } else if (result.action === "VERIFIED") {
359
- console.log(`⚠️ VERIFIED — approved, but the automatic payout did NOT go through. No funds moved.`);
360
- } else if (result.timedOut === true) {
361
- console.log(`⏳ PENDING_REVIEW — the panel did not answer (too few models). NOT a rejection; a human decides.`);
362
- } else if (result.action === "PENDING_REVIEW") {
363
- console.log(`⏳ PENDING_REVIEW — ${result.confidence}% confidence, manual review required.`);
364
- } else if (result.action === "REJECTED") {
365
- console.log(`❌ REJECTED — ${result.confidence}% confidence.`);
366
- } else {
367
- console.log(`⚠️ Outcome not recognised by this version (action: ${result.action}). No conclusion drawn.`);
368
- }
369
- console.log(` Decision: ${result.decision ?? "none read (the stream dropped before it)"}`);
370
- console.log(` View contract: ${BASE_URL}/contract/${invite.contractId}`);
371
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
372
- }
373
-
374
- async function getAgentId() {
375
- const data = await apiGet("/api/agent/me");
376
- return data.agentId;
377
- }
378
-
379
- async function main() {
380
- const agentId = await getAgentId();
381
- console.log(`\n🤖 Cascrow Builder Agent started`);
382
- console.log(` Agent ID: ${agentId}`);
383
- console.log(` Share this ID with the Requester agent`);
384
- console.log(` Polling for work every ${POLL_INTERVAL_MS / 1000}s...\n`);
385
-
386
- // Register EVM wallet address so Requester can fund real escrow to this agent.
387
- //
388
- // POST /api/user/wallet proves key possession: it wants a challenge token from
389
- // GET /api/user/wallet/challenge plus a signature over the nonce it carries.
390
- // This CLI is deliberately dependency-free and Node's crypto cannot produce an
391
- // EIP-191 signature (no keccak256, no recovery id), so it cannot complete that
392
- // flow on its own — and posting the address alone has been answered with 400
393
- // since the check went in. Say so plainly instead of warning past it: a
394
- // builder whose wallet is not linked is not discoverable and cannot be paid,
395
- // which is worth more than one line of yellow text.
396
- if (AGENT_EVM_WALLET) {
397
- try {
398
- await apiPost("/api/user/wallet", { walletAddress: AGENT_EVM_WALLET });
399
- console.log(` 💳 EVM wallet registered: ${AGENT_EVM_WALLET}\n`);
400
- } catch (e) {
401
- console.warn(
402
- ` ⚠️ Wallet NOT linked (${e.message}).\n` +
403
- ` ${AGENT_EVM_WALLET} is unusable until it is: escrow cannot be funded to it\n` +
404
- ` and this agent will not appear in /api/agent/discover.\n` +
405
- ` Link it once, from a signer that holds the key:\n` +
406
- ` 1. GET ${BASE_URL}/api/user/wallet/challenge (Bearer <your API key>)\n` +
407
- ` 2. sign the nonce in the returned token with the wallet's key\n` +
408
- ` 3. POST ${BASE_URL}/api/user/wallet { walletAddress, signature, token }\n`
409
- );
410
- }
411
- }
412
-
413
- while (true) {
414
- try {
415
- const data = await apiGet("/api/agent/pending-invites");
416
- const invites = data.invites ?? [];
417
- for (const invite of invites) {
418
- await handleInvite(invite);
419
- }
420
- } catch (err) {
421
- console.error(`[poll error] ${err.message}`);
422
- }
423
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
424
- }
425
- }
426
-
427
- main().catch((err) => {
428
- console.error("Fatal:", err.message);
429
- process.exit(1);
430
- });