cascrow-cli 1.2.2 → 1.3.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/README.md +26 -3
- package/builder-agent.js +135 -19
- package/index.js +325 -19
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cascrow-cli
|
|
2
2
|
|
|
3
|
-
Command-line interface for [Cascrow](https://cascrow.com) —
|
|
3
|
+
Command-line interface for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy (AI verification, on-chain reputation, milestone escrow) on the XRP Ledger, for shells, CI pipelines, and subprocesses.
|
|
4
4
|
|
|
5
5
|
AI agents and their operators use Cascrow to create milestone-based contracts, submit proof of completion, and get verified by a **5-model AI majority vote** — with funds released automatically on-chain. No humans required.
|
|
6
6
|
|
|
@@ -38,7 +38,10 @@ cascrow verify-work --task "Build a landing page with a working contact form" \
|
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Returns a decision, confidence score, per-model reasoning, and a public shareable report URL — no
|
|
41
|
-
contract, no escrow, no wallet.
|
|
41
|
+
contract, no escrow, no wallet. If `CASCROW_API_KEY` happens to be set in your environment, `verify-work`
|
|
42
|
+
picks it up too (it's optional, not ignored) and the verdict is additionally anchored on the XRP Ledger
|
|
43
|
+
mainnet — a commitment hash you can independently recompute from the report. Without a key, the verdict
|
|
44
|
+
is never anchored.
|
|
42
45
|
|
|
43
46
|
## All Commands
|
|
44
47
|
|
|
@@ -55,6 +58,8 @@ contract, no escrow, no wallet.
|
|
|
55
58
|
| `join` | Join a contract as Builder via invite code | Yes |
|
|
56
59
|
| `handoff` | Send a contract invite to a Builder agent by Agent ID | Yes |
|
|
57
60
|
| `check-invites` | Check pending contract invites for this agent | Yes |
|
|
61
|
+
| `review-queue` | As funder: list milestones still waiting for your verdict | Yes |
|
|
62
|
+
| `review` | As funder: approve or reject a milestone under manual review | Yes |
|
|
58
63
|
| `me` | Print this agent's ID | Yes |
|
|
59
64
|
|
|
60
65
|
Run `cascrow --help` (or `cascrow`) any time for the full, always-current option reference.
|
|
@@ -109,7 +114,7 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
|
|
|
109
114
|
```
|
|
110
115
|
--contract <id> required
|
|
111
116
|
--private-key <key> required EVM private key (funds come from this wallet)
|
|
112
|
-
--amount <usd>
|
|
117
|
+
--amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
|
|
113
118
|
--milestone <id> optional
|
|
114
119
|
```
|
|
115
120
|
|
|
@@ -146,6 +151,24 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
|
|
|
146
151
|
|
|
147
152
|
No options.
|
|
148
153
|
|
|
154
|
+
### `review-queue`
|
|
155
|
+
|
|
156
|
+
No options. Lists the contracts on which you, as funder, still owe a manual
|
|
157
|
+
verdict — the pull behind the `manual_review.required` webhook. A webhook nobody
|
|
158
|
+
received is a decision nobody makes, and cascrow auto-approves an undecided
|
|
159
|
+
milestone after 48 hours (agent contracts) or 14 days (human) and releases the
|
|
160
|
+
escrow, so poll this whenever you have funded work in flight.
|
|
161
|
+
|
|
162
|
+
### `review`
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
--contract <id> required from `cascrow review-queue`
|
|
166
|
+
--decision <APPROVE|REJECT> required APPROVE releases the escrow; REJECT returns
|
|
167
|
+
the milestone to the builder, who can resubmit
|
|
168
|
+
--milestone <id> required once more than one milestone of the contract is
|
|
169
|
+
pending review; `review-queue` names it
|
|
170
|
+
```
|
|
171
|
+
|
|
149
172
|
## Environment
|
|
150
173
|
|
|
151
174
|
| Variable | Description |
|
package/builder-agent.js
CHANGED
|
@@ -50,25 +50,62 @@ async function uploadProof(milestoneId, content) {
|
|
|
50
50
|
return data;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// SEC-073. The FIFTH client door of `/api/verify`, and until 2026-09-22 the only
|
|
54
|
+
// one with no deadline at all: it was invisible to `latency-budget.test.ts`,
|
|
55
|
+
// whose client list was three hand-typed files — and this file lives inside one
|
|
56
|
+
// of the three packages already on that list, so the subject was PACKAGES where
|
|
57
|
+
// the population is FILES (F-30).
|
|
58
|
+
//
|
|
59
|
+
// 235_000 — one step below the route's 240 s `maxDuration`, NOT "above the
|
|
60
|
+
// worst case": the tail (pre-stream + a 15 s blob read + the 205 s ladder + the
|
|
61
|
+
// bounded EVM retry) reaches ~239 s, so no value is both above it and below the
|
|
62
|
+
// cap. See VERIFY_TAIL_WORST_CASE_MS in src/lib/latency-budget.ts. Cleared after
|
|
63
|
+
// the READ LOOP rather than at the `fetch`: `fetch` resolves on response headers about 12 s in, and the whole
|
|
64
|
+
// retry ladder arrives afterwards out of `res.body`.
|
|
53
65
|
async function streamVerify(proofId) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), 235_000);
|
|
68
|
+
let res;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetch(`${BASE_URL}/api/verify`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Authorization": `Bearer ${API_KEY}`,
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"Accept": "text/event-stream",
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ proofId }),
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
} catch (e) {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
// SEC-073. Now that the deadline is live it can actually fire, and a raw
|
|
83
|
+
// `DOMException: This operation was aborted` names no remedy — worse, per
|
|
84
|
+
// the tail arithmetic in latency-budget.ts the abort can land AFTER
|
|
85
|
+
// releaseMilestone(), so "it failed" may be false about the money.
|
|
86
|
+
if (e && e.name === "AbortError") {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Verification is still running on the server — this client stopped waiting, it did not fail. " +
|
|
89
|
+
`Poll GET /api/agent/proof-status/${proofId} for the verdict.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
63
94
|
if (!res.ok) {
|
|
95
|
+
clearTimeout(timeout);
|
|
64
96
|
const data = await res.json().catch(() => ({}));
|
|
65
97
|
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
66
98
|
}
|
|
99
|
+
if (!res.body) {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
throw new Error("Verification stream had no body");
|
|
102
|
+
}
|
|
67
103
|
|
|
68
104
|
const reader = res.body.getReader();
|
|
69
105
|
const decoder = new TextDecoder();
|
|
70
106
|
let buffer = "";
|
|
71
107
|
|
|
108
|
+
try {
|
|
72
109
|
while (true) {
|
|
73
110
|
const { done, value } = await reader.read();
|
|
74
111
|
if (done) break;
|
|
@@ -95,6 +132,21 @@ async function streamVerify(proofId) {
|
|
|
95
132
|
}
|
|
96
133
|
}
|
|
97
134
|
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// The deadline can now fire DURING the read — that is the whole point of
|
|
137
|
+
// clearing it below the loop rather than at the fetch. Same translation as
|
|
138
|
+
// the fetch-side catch above.
|
|
139
|
+
if (e && e.name === "AbortError") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Verification is still running on the server — this client stopped waiting, it did not fail. " +
|
|
142
|
+
`Poll GET /api/agent/proof-status/${proofId} for the verdict.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
throw e;
|
|
146
|
+
} finally {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
reader.cancel().catch(() => {});
|
|
149
|
+
}
|
|
98
150
|
throw new Error("Stream ended without complete event");
|
|
99
151
|
}
|
|
100
152
|
|
|
@@ -173,25 +225,72 @@ VERIFICATION CRITERIA MET:
|
|
|
173
225
|
// The verification is still running server-side — just poll for the result,
|
|
174
226
|
// do NOT call streamVerify again (would trigger duplicate AI verification).
|
|
175
227
|
console.log(` ⚠️ Stream ended early, polling for result...`);
|
|
228
|
+
// SEC-076. This polled `contract.status`, and that column answers a different
|
|
229
|
+
// question than the one asked here. It is a single slot with nine writers:
|
|
230
|
+
// funding a SIBLING milestone writes `contractStatusAfterFunding(...)` over
|
|
231
|
+
// it under `status: { notIn: ["COMPLETED","EXPIRED"] }`, so it fails both
|
|
232
|
+
// ways. False negative: the pin is overwritten with FUNDED, no terminal value
|
|
233
|
+
// ever appears, and this loop dies after five minutes on "Timed out waiting
|
|
234
|
+
// for verification result" for a proof that was in fact decided. False
|
|
235
|
+
// positive: the contract reads VERIFIED/COMPLETED because ANOTHER milestone
|
|
236
|
+
// settled, and the builder is handed a verdict that is not their proof's -
|
|
237
|
+
// `decision` below is derived from it.
|
|
238
|
+
//
|
|
239
|
+
// `GET /api/contracts/{id}` returns `milestones: [{ id, status, … }]`, and we
|
|
240
|
+
// funded a specific one, so the honest read is that row. The contract-level
|
|
241
|
+
// read is kept only for the legacy population that has no milestone rows at
|
|
242
|
+
// all (proofs written via the contractId-only path).
|
|
243
|
+
const TERMINAL_PROOF_STATES = ["VERIFIED", "COMPLETED", "REJECTED", "PENDING_REVIEW"];
|
|
176
244
|
for (let i = 0; i < 60; i++) {
|
|
177
245
|
await new Promise((r) => setTimeout(r, 5000));
|
|
178
246
|
const contract = await apiGet(`/api/contracts/${joined.contractId}`);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
247
|
+
const mine = (contract.milestones ?? []).find((m) => m.id === fundedMilestoneId);
|
|
248
|
+
const observed = mine ? mine.status : (contract.milestones ?? []).length === 0 ? contract.status : null;
|
|
249
|
+
if (observed && TERMINAL_PROOF_STATES.includes(observed)) {
|
|
250
|
+
const decision = ["VERIFIED", "COMPLETED"].includes(observed) ? "YES" : "NO";
|
|
251
|
+
result = { decision, confidence: 0, action: observed };
|
|
252
|
+
console.log(` ✓ Got result via polling: ${observed}`);
|
|
183
253
|
break;
|
|
184
254
|
}
|
|
185
255
|
}
|
|
186
256
|
if (!result) throw new Error("Timed out waiting for verification result");
|
|
187
257
|
}
|
|
188
258
|
|
|
189
|
-
|
|
259
|
+
// SEC-055/058/059. This re-implemented the three-tier rule from the PROOF's verdict
|
|
260
|
+
// (`decision === "YES" && confidence >= 85`), so `COMPLETED`, `SUPERSEDED`, a
|
|
261
|
+
// compliance block and a release hold all printed "✅ VERIFIED", and a panel outage
|
|
262
|
+
// (`decision: "PENDING"`, confidence 0) printed "❌ NOT VERIFIED — 0% confidence" —
|
|
263
|
+
// our own outage read back to the builder as a rejection of their work.
|
|
264
|
+
//
|
|
265
|
+
// `passed` now means FUNDS MOVED, and only `COMPLETED` says that. The polling
|
|
266
|
+
// fallback above already derives `action` from the funded MILESTONE's status
|
|
267
|
+
// (SEC-076; it read the contract row until 2026-09-22), so both paths reach this
|
|
268
|
+
// with the same vocabulary.
|
|
269
|
+
const released = result.action === "COMPLETED";
|
|
270
|
+
const passed = released;
|
|
190
271
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
191
|
-
if (
|
|
192
|
-
console.log(
|
|
272
|
+
if (result.action === "SUPERSEDED") {
|
|
273
|
+
console.log(`ℹ️ SUPERSEDED — another proof on this milestone was decided first; this changed nothing.`);
|
|
274
|
+
} else if (released) {
|
|
275
|
+
console.log(
|
|
276
|
+
result.alreadyReleased === true
|
|
277
|
+
? `✅ COMPLETED — already released by another request. The funds are out; do not resubmit.`
|
|
278
|
+
: `✅ COMPLETED — ${result.confidence}% confidence, funds released.`
|
|
279
|
+
);
|
|
280
|
+
} else if (result.complianceBlocked === true) {
|
|
281
|
+
console.log(`⏸️ VERIFIED — payout on hold pending compliance review. No funds moved.`);
|
|
282
|
+
} else if (result.releaseHoldUntil) {
|
|
283
|
+
console.log(`⏸️ VERIFIED — approved, payout scheduled for ${result.releaseHoldUntil}. NO FUNDS HAVE MOVED YET.`);
|
|
284
|
+
} else if (result.action === "VERIFIED") {
|
|
285
|
+
console.log(`⚠️ VERIFIED — approved, but the automatic payout did NOT go through. No funds moved.`);
|
|
286
|
+
} else if (result.timedOut === true) {
|
|
287
|
+
console.log(`⏳ PENDING_REVIEW — the panel did not answer (too few models). NOT a rejection; a human decides.`);
|
|
288
|
+
} else if (result.action === "PENDING_REVIEW") {
|
|
289
|
+
console.log(`⏳ PENDING_REVIEW — ${result.confidence}% confidence, manual review required.`);
|
|
290
|
+
} else if (result.action === "REJECTED") {
|
|
291
|
+
console.log(`❌ REJECTED — ${result.confidence}% confidence.`);
|
|
193
292
|
} else {
|
|
194
|
-
console.log(
|
|
293
|
+
console.log(`⚠️ Outcome not recognised by this version (action: ${result.action}). No conclusion drawn.`);
|
|
195
294
|
}
|
|
196
295
|
console.log(` Decision: ${result.decision}`);
|
|
197
296
|
console.log(` View contract: ${BASE_URL}/contract/${invite.contractId}`);
|
|
@@ -210,13 +309,30 @@ async function main() {
|
|
|
210
309
|
console.log(` Share this ID with the Requester agent`);
|
|
211
310
|
console.log(` Polling for work every ${POLL_INTERVAL_MS / 1000}s...\n`);
|
|
212
311
|
|
|
213
|
-
// Register EVM wallet address so Requester can fund real escrow to this agent
|
|
312
|
+
// Register EVM wallet address so Requester can fund real escrow to this agent.
|
|
313
|
+
//
|
|
314
|
+
// POST /api/user/wallet proves key possession: it wants a challenge token from
|
|
315
|
+
// GET /api/user/wallet/challenge plus a signature over the nonce it carries.
|
|
316
|
+
// This CLI is deliberately dependency-free and Node's crypto cannot produce an
|
|
317
|
+
// EIP-191 signature (no keccak256, no recovery id), so it cannot complete that
|
|
318
|
+
// flow on its own — and posting the address alone has been answered with 400
|
|
319
|
+
// since the check went in. Say so plainly instead of warning past it: a
|
|
320
|
+
// builder whose wallet is not linked is not discoverable and cannot be paid,
|
|
321
|
+
// which is worth more than one line of yellow text.
|
|
214
322
|
if (AGENT_EVM_WALLET) {
|
|
215
323
|
try {
|
|
216
324
|
await apiPost("/api/user/wallet", { walletAddress: AGENT_EVM_WALLET });
|
|
217
325
|
console.log(` 💳 EVM wallet registered: ${AGENT_EVM_WALLET}\n`);
|
|
218
326
|
} catch (e) {
|
|
219
|
-
console.warn(
|
|
327
|
+
console.warn(
|
|
328
|
+
` ⚠️ Wallet NOT linked (${e.message}).\n` +
|
|
329
|
+
` ${AGENT_EVM_WALLET} is unusable until it is: escrow cannot be funded to it\n` +
|
|
330
|
+
` and this agent will not appear in /api/agent/discover.\n` +
|
|
331
|
+
` Link it once, from a signer that holds the key:\n` +
|
|
332
|
+
` 1. GET ${BASE_URL}/api/user/wallet/challenge (Bearer <your API key>)\n` +
|
|
333
|
+
` 2. sign the nonce in the returned token with the wallet's key\n` +
|
|
334
|
+
` 3. POST ${BASE_URL}/api/user/wallet { walletAddress, signature, token }\n`
|
|
335
|
+
);
|
|
220
336
|
}
|
|
221
337
|
}
|
|
222
338
|
|
package/index.js
CHANGED
|
@@ -33,6 +33,18 @@ async function apiPost(path, body, key = API_KEY) {
|
|
|
33
33
|
return data;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// SEC-078. A failed standalone verification still has a row, and the route answers
|
|
37
|
+
// the 500 with `id`, `publicHash` and `reportUrl` so the caller who paid for it can name it. The
|
|
38
|
+
// CLI only prints a string, so the reference goes into the string. Both request
|
|
39
|
+
// shapes (multipart image, JSON) throw through here.
|
|
40
|
+
function standaloneFailure(status, data) {
|
|
41
|
+
const d = data ?? {};
|
|
42
|
+
const base = typeof d.error === "string" ? d.error : `HTTP ${status}`;
|
|
43
|
+
const report = typeof d.reportUrl === "string" ? d.reportUrl : typeof d.publicHash === "string" ? d.publicHash : null;
|
|
44
|
+
const ref = typeof d.id === "string" && report ? ` (verification id: ${d.id}, report: ${report})` : "";
|
|
45
|
+
return new Error(base + ref);
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
async function apiGet(path, key = API_KEY) {
|
|
37
49
|
const res = await fetch(`${BASE_URL}${path}`, {
|
|
38
50
|
headers: { ...authHeader(key) },
|
|
@@ -60,8 +72,11 @@ async function uploadProof(milestoneId, filename, content, key = API_KEY) {
|
|
|
60
72
|
// Stream verify via SSE — used by the legacy two-step `submit` + `verify --proof <id>` flow
|
|
61
73
|
async function streamVerify(proofId, key = API_KEY) {
|
|
62
74
|
const controller = new AbortController();
|
|
63
|
-
//
|
|
64
|
-
|
|
75
|
+
// 235s: one step below the 240s maxDuration of /api/verify, not above its
|
|
76
|
+
// worst case — the tail reaches ~239s, so no value is both. See
|
|
77
|
+
// VERIFY_TAIL_WORST_CASE_MS in src/lib/latency-budget.ts. Cleared after the read loop, not at the fetch
|
|
78
|
+
// (SEC-073), which is what makes the number mean anything.
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), 235_000);
|
|
65
80
|
|
|
66
81
|
let res;
|
|
67
82
|
try {
|
|
@@ -75,14 +90,35 @@ async function streamVerify(proofId, key = API_KEY) {
|
|
|
75
90
|
body: JSON.stringify({ proofId }),
|
|
76
91
|
signal: controller.signal,
|
|
77
92
|
});
|
|
78
|
-
}
|
|
93
|
+
} catch (e) {
|
|
79
94
|
clearTimeout(timeout);
|
|
95
|
+
// SEC-073. Now that the deadline is live it can actually fire, and a raw
|
|
96
|
+
// `DOMException: This operation was aborted` names no remedy — worse, per
|
|
97
|
+
// the tail arithmetic in latency-budget.ts the abort can land AFTER
|
|
98
|
+
// releaseMilestone(), so "it failed" may be false about the money.
|
|
99
|
+
if (e && e.name === "AbortError") {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Verification is still running on the server — this client stopped waiting, it did not fail. " +
|
|
102
|
+
`Poll GET /api/agent/proof-status/${proofId} for the verdict.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
throw e;
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
if (!res.ok) {
|
|
109
|
+
// SEC-073, second pass. Replacing the fetch's `finally` with a `catch` left
|
|
110
|
+
// THIS exit uncovered: a 409 from /api/verify (already verified, panel in
|
|
111
|
+
// progress, resubmit limit) returned with a 235 s timer still pending. In a
|
|
112
|
+
// long-lived MCP server that holds the event loop and later aborts a fetch
|
|
113
|
+
// that finished minutes ago.
|
|
114
|
+
clearTimeout(timeout);
|
|
83
115
|
const data = await res.json().catch(() => ({}));
|
|
84
116
|
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
85
117
|
}
|
|
118
|
+
if (!res.body) {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
throw new Error("Verification stream had no body");
|
|
121
|
+
}
|
|
86
122
|
|
|
87
123
|
const votes = [];
|
|
88
124
|
const reader = res.body.getReader();
|
|
@@ -117,15 +153,127 @@ async function streamVerify(proofId, key = API_KEY) {
|
|
|
117
153
|
}
|
|
118
154
|
}
|
|
119
155
|
}
|
|
156
|
+
// SEC-073. Cleared HERE, after the read loop — not in a `finally` around the
|
|
157
|
+
// `fetch`, which resolves on response HEADERS about 12 s into `/api/verify`
|
|
158
|
+
// while the whole 205 s retry ladder arrives afterwards out of `res.body`.
|
|
159
|
+
// At the fetch this was a time-to-first-byte deadline over a twelve-second
|
|
160
|
+
// phase, so its value bounded nothing and could sit below the server's worst
|
|
161
|
+
// case unnoticed. CLIENT_ABORT_MS was raised to 235_000 in the same commit.
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// The deadline can now fire DURING the read — that is the whole point of
|
|
164
|
+
// clearing it below the loop rather than at the fetch. Same translation as
|
|
165
|
+
// the fetch-side catch above.
|
|
166
|
+
if (e && e.name === "AbortError") {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Verification is still running on the server — this client stopped waiting, it did not fail. " +
|
|
169
|
+
`Poll GET /api/agent/proof-status/${proofId} for the verdict.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
throw e;
|
|
120
173
|
} finally {
|
|
174
|
+
clearTimeout(timeout);
|
|
121
175
|
reader.cancel().catch(() => {});
|
|
122
176
|
}
|
|
123
177
|
|
|
124
178
|
throw new Error("Stream ended without a complete event");
|
|
125
179
|
}
|
|
126
180
|
|
|
181
|
+
/**
|
|
182
|
+
* One human-readable line for a verification `complete` frame.
|
|
183
|
+
*
|
|
184
|
+
* Every arm says whether FUNDS MOVED, because that is the one thing a caller cannot
|
|
185
|
+
* find out any other way. `action === "VERIFIED"` alone never means they did: a hold and
|
|
186
|
+
* a compliance block carry their own flag, a success arrives as `COMPLETED`, and a BARE
|
|
187
|
+
* `VERIFIED` is reachable only when the automatic release was attempted and FAILED.
|
|
188
|
+
*/
|
|
189
|
+
function outcomeLine(r) {
|
|
190
|
+
const c = r.confidence ?? 0;
|
|
191
|
+
if (r.action === "SUPERSEDED") {
|
|
192
|
+
return "ℹ️ SUPERSEDED — another proof on this milestone was decided first, so this verification changed nothing. Your evidence was recorded.";
|
|
193
|
+
}
|
|
194
|
+
if (r.action === "COMPLETED") {
|
|
195
|
+
return r.alreadyReleased === true
|
|
196
|
+
? "✅ COMPLETED — this milestone was already released by another request. The funds are out; do not resubmit."
|
|
197
|
+
: `✅ COMPLETED (${c}% confidence) — funds released${r.txHash ? `, tx ${r.txHash}` : ""}.`;
|
|
198
|
+
}
|
|
199
|
+
if (r.complianceBlocked === true) {
|
|
200
|
+
return `⏸️ VERIFIED (${c}%) — payout on hold pending compliance review. No funds moved.`;
|
|
201
|
+
}
|
|
202
|
+
if (r.releaseHoldUntil) {
|
|
203
|
+
return `⏸️ VERIFIED (${c}% confidence) — approved, payout scheduled for ${r.releaseHoldUntil}. NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move.`;
|
|
204
|
+
}
|
|
205
|
+
if (r.action === "VERIFIED") {
|
|
206
|
+
return `⚠️ VERIFIED (${c}% confidence) — the work was approved, but the automatic payout did NOT go through. No funds have moved; the release can be retried.`;
|
|
207
|
+
}
|
|
208
|
+
if (r.timedOut === true) {
|
|
209
|
+
return "⏳ 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.";
|
|
210
|
+
}
|
|
211
|
+
if (r.action === "PENDING_REVIEW") {
|
|
212
|
+
return `⏳ PENDING_REVIEW (${c}%) — manual review required.`;
|
|
213
|
+
}
|
|
214
|
+
if (r.action === "REJECTED") {
|
|
215
|
+
return `❌ REJECTED (${c}%) — resubmit with stronger proof.`;
|
|
216
|
+
}
|
|
217
|
+
return `⚠️ This verification finished with an outcome this CLI does not recognise (action: ${r.action}). NO conclusion was drawn — check the milestone status and upgrade cascrow-cli.`;
|
|
218
|
+
}
|
|
219
|
+
|
|
127
220
|
// ─── Output helpers ───────────────────────────────────────────────────────────
|
|
128
221
|
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One `verify/standalone` call under a client deadline (SEC-073).
|
|
225
|
+
*
|
|
226
|
+
* The route declares `maxDuration = 120`; without this the only signal a hung
|
|
227
|
+
* call gives is the platform tearing the connection down, which reaches the
|
|
228
|
+
* caller as a transport error with nothing in it to act on. One function rather
|
|
229
|
+
* than a copy per shape: the two fetches it wraps are twenty lines apart in the
|
|
230
|
+
* same handler, and a guard reading either in isolation would have reported the
|
|
231
|
+
* pair as protected (F-12/F-60).
|
|
232
|
+
*
|
|
233
|
+
* Mirrors `STANDALONE_CLIENT_ABORT_MS` in `src/lib/latency-budget.ts`. A
|
|
234
|
+
* published package cannot import from the app, so this is a copy by necessity;
|
|
235
|
+
* `standalone-client-deadline.test.ts` is what keeps the FOUR copies equal — this
|
|
236
|
+
* file, the other published package, the SDK, and the owner in
|
|
237
|
+
* `src/lib/latency-budget.ts`.
|
|
238
|
+
*/
|
|
239
|
+
const STANDALONE_CLIENT_ABORT_MS = 115_000;
|
|
240
|
+
|
|
241
|
+
async function withStandaloneDeadline(run) {
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
const timer = setTimeout(() => controller.abort(), STANDALONE_CLIENT_ABORT_MS);
|
|
244
|
+
try {
|
|
245
|
+
return await run(controller.signal);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (err && err.name === "AbortError") {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Verification did not answer within ${Math.round(STANDALONE_CLIENT_ABORT_MS / 1000)}s. ` +
|
|
250
|
+
"The server may still be working on it — retry, and if it keeps timing out the panel is degraded."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
throw err;
|
|
254
|
+
} finally {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* MACHINE OUTPUT ON stdout, and that is the point of this comment.
|
|
261
|
+
*
|
|
262
|
+
* `SEC-116-INVENTORY` exempted this CLI with the reason "schreibt nach stdout/stderr
|
|
263
|
+
* für einen Menschen". That is true of `err()` below and of the human summaries — and
|
|
264
|
+
* FALSE of this function, which emits `JSON.stringify` for the "CI pipelines and
|
|
265
|
+
* subprocesses" the README names as the target. An exemption whose reason holds for
|
|
266
|
+
* one half of its own subject is F-70, and it is recorded here rather than left in a
|
|
267
|
+
* document nobody reads next to the code.
|
|
268
|
+
*
|
|
269
|
+
* No marker is added, and that is a decision rather than an omission: this CLI's JSON
|
|
270
|
+
* is a published output shape, and prefixing fields inside it would change what every
|
|
271
|
+
* existing parser reads. The honest statement instead — any free text in this output
|
|
272
|
+
* (`reasoning`, `summary`, per-model votes) is MODEL OUTPUT over evidence a
|
|
273
|
+
* counterparty submitted, and reaches you RAW. If you pipe it into a model, treat it
|
|
274
|
+
* as data and not as instructions; the marked surfaces are the two MCP servers and the
|
|
275
|
+
* `/api/agent/verify/*` routes.
|
|
276
|
+
*/
|
|
129
277
|
function print(obj) {
|
|
130
278
|
console.log(JSON.stringify(obj, null, 2));
|
|
131
279
|
}
|
|
@@ -154,6 +302,8 @@ Commands:
|
|
|
154
302
|
join Join a contract as Builder via invite code
|
|
155
303
|
handoff Send a contract invite to a Builder agent by Agent ID
|
|
156
304
|
check-invites Check pending contract invites for this agent
|
|
305
|
+
review-queue As funder: list milestones still waiting for your verdict
|
|
306
|
+
review As funder: approve or reject a milestone under manual review
|
|
157
307
|
me Print this agent's ID
|
|
158
308
|
|
|
159
309
|
Options for each command:
|
|
@@ -189,7 +339,7 @@ Options for each command:
|
|
|
189
339
|
escrow-fund [REAL on-chain RLUSD — agent's own wallet]
|
|
190
340
|
--contract <id> required
|
|
191
341
|
--private-key <key> required EVM private key (funds come from this wallet)
|
|
192
|
-
--amount <usd>
|
|
342
|
+
--amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
|
|
193
343
|
--milestone <id> optional
|
|
194
344
|
|
|
195
345
|
submit [legacy: use verify instead]
|
|
@@ -242,6 +392,13 @@ Examples:
|
|
|
242
392
|
# (Builder agent runs:)
|
|
243
393
|
cascrow check-invites
|
|
244
394
|
cascrow join --invite <code>
|
|
395
|
+
|
|
396
|
+
# As the funder: decide what the AI panel could not (confidence 60-85)
|
|
397
|
+
# Poll this — a manual_review.required webhook you missed is a decision nobody
|
|
398
|
+
# makes, and cascrow auto-approves an undecided milestone after 48h.
|
|
399
|
+
cascrow review-queue
|
|
400
|
+
cascrow review --contract <contractId> --decision APPROVE
|
|
401
|
+
cascrow review --contract <contractId> --decision REJECT --milestone <milestoneId>
|
|
245
402
|
`);
|
|
246
403
|
process.exit(0);
|
|
247
404
|
}
|
|
@@ -317,18 +474,46 @@ async function cmdVerifyWork(args) {
|
|
|
317
474
|
form.append("taskDescription", args.task);
|
|
318
475
|
form.append("image", new Blob([imageBuffer], { type: imageMimeType }), args.image.split("/").pop());
|
|
319
476
|
|
|
320
|
-
const res = await
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
477
|
+
const { res, data: body } = await withStandaloneDeadline(async (signal) => {
|
|
478
|
+
const r = await fetch(`${BASE_URL}/api/verify/standalone`, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: { ...authHeader(API_KEY) },
|
|
481
|
+
body: form,
|
|
482
|
+
signal,
|
|
483
|
+
});
|
|
484
|
+
// The body is read INSIDE the deadline. A response whose headers arrived
|
|
485
|
+
// and whose body then stalls is otherwise unbounded — a deadline that ends
|
|
486
|
+
// at the headers bounds time-to-first-byte and nothing else (SEC-073).
|
|
487
|
+
return { res: r, data: await r.json() };
|
|
324
488
|
});
|
|
325
|
-
|
|
326
|
-
|
|
489
|
+
if (!res.ok) throw standaloneFailure(res.status, body);
|
|
490
|
+
// Assigned to the OUTER `data`, not shadowing it: `const { data }` inside the
|
|
491
|
+
// branch left the function-scoped variable undefined and the next line read
|
|
492
|
+
// `data.decision` off it. `node --check` accepts a shadow happily.
|
|
493
|
+
data = body;
|
|
327
494
|
} else {
|
|
328
495
|
const body = { taskDescription: args.task };
|
|
329
496
|
if (args.pr) body.prUrl = args.pr;
|
|
330
497
|
if (args.code) body.codeText = args.code;
|
|
331
|
-
|
|
498
|
+
// Not `apiPost`: its generic throw drops the row reference the 500 carries.
|
|
499
|
+
const { res, data: answer } = await withStandaloneDeadline(async (signal) => {
|
|
500
|
+
const r = await fetch(`${BASE_URL}/api/verify/standalone`, {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: { ...authHeader(API_KEY), "Content-Type": "application/json" },
|
|
503
|
+
body: JSON.stringify(body),
|
|
504
|
+
signal,
|
|
505
|
+
});
|
|
506
|
+
// The body is read INSIDE the deadline. A response whose headers arrived
|
|
507
|
+
// and whose body then stalls is otherwise unbounded — a deadline that ends
|
|
508
|
+
// at the headers bounds time-to-first-byte and nothing else (SEC-073).
|
|
509
|
+
return { res: r, data: await r.json() };
|
|
510
|
+
});
|
|
511
|
+
if (!res.ok) throw standaloneFailure(res.status, answer);
|
|
512
|
+
// Assigned to the OUTER `data`, not shadowing it: `const { data }` inside the
|
|
513
|
+
// branch left the function-scoped variable undefined and the next line read
|
|
514
|
+
// `data.decision` off it. `answer` rather than `body` because this branch
|
|
515
|
+
// already has a `body` — the REQUEST payload — twelve lines up.
|
|
516
|
+
data = answer;
|
|
332
517
|
}
|
|
333
518
|
|
|
334
519
|
const votes = Array.isArray(data.modelVotes) ? data.modelVotes : [];
|
|
@@ -359,25 +544,48 @@ async function cmdVerifyWork(args) {
|
|
|
359
544
|
// Auto-releases funds and mints NFT on approval — no separate `verify --proof <id>` needed.
|
|
360
545
|
async function cmdVerify(args) {
|
|
361
546
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
362
|
-
if (!args.contract) err("--contract is required");
|
|
363
547
|
if (!args.proof) err("--proof is required");
|
|
364
548
|
|
|
365
|
-
// Legacy mode: --proof looks like a proof ID (prf_...) and no --contract — use stream verify
|
|
549
|
+
// Legacy mode: --proof looks like a proof ID (prf_...) and no --contract — use stream verify.
|
|
550
|
+
// Checked BEFORE the --contract requirement below: that check used to run
|
|
551
|
+
// first and process.exit(1) whenever --contract was missing, which made
|
|
552
|
+
// this branch (and streamVerify()) permanently unreachable — exactly the
|
|
553
|
+
// invocation cmdSubmit's own output tells users to run.
|
|
366
554
|
if (!args.contract && args.proof && !args.proof.includes(" ")) {
|
|
367
555
|
process.stderr.write("\nRunning AI verification (5 models)...\n\n");
|
|
368
556
|
const result = await streamVerify(args.proof);
|
|
369
|
-
|
|
557
|
+
// SEC-055/058/059 at the third published surface. `passed` used to be
|
|
558
|
+
// `decision === "YES" && confidence >= 85`, i.e. a re-implementation of the
|
|
559
|
+
// three-tier rule from the PROOF's verdict — which answers a different question
|
|
560
|
+
// from the one a caller is asking. `COMPLETED`, `SUPERSEDED`, a compliance block
|
|
561
|
+
// and a release hold all rendered as verified, and a panel outage
|
|
562
|
+
// (`decision: "PENDING"`) rendered as "NOT VERIFIED 0%", i.e. our own outage read
|
|
563
|
+
// back to the builder as a rejection.
|
|
564
|
+
//
|
|
565
|
+
// `passed` now means MONEY MOVED, and only `COMPLETED` says that.
|
|
566
|
+
const released = result.action === "COMPLETED";
|
|
567
|
+
const passed = released;
|
|
370
568
|
print({
|
|
371
569
|
decision: result.decision,
|
|
372
570
|
confidence: result.confidence,
|
|
373
571
|
passed,
|
|
374
572
|
action: result.action,
|
|
573
|
+
released,
|
|
574
|
+
alreadyReleased: result.alreadyReleased === true,
|
|
575
|
+
superseded: result.action === "SUPERSEDED",
|
|
576
|
+
complianceBlocked: result.complianceBlocked === true,
|
|
577
|
+
releaseHoldUntil: result.releaseHoldUntil ?? null,
|
|
578
|
+
panelUnavailable: result.timedOut === true,
|
|
579
|
+
txHash: result.txHash ?? null,
|
|
375
580
|
reasoning: result.reasoning,
|
|
376
581
|
modelVotes: (result.votes ?? []).map(v => ({ model: v.model, decision: v.decision, confidence: v.confidence })),
|
|
377
582
|
});
|
|
583
|
+
process.stderr.write(outcomeLine(result) + "\n");
|
|
378
584
|
return;
|
|
379
585
|
}
|
|
380
586
|
|
|
587
|
+
if (!args.contract) err("--contract is required");
|
|
588
|
+
|
|
381
589
|
// One-shot mode: text proof → /api/mcp/submit → verdict + auto-release
|
|
382
590
|
const links = args.links
|
|
383
591
|
? args.links.split(",").map(l => l.trim()).filter(Boolean)
|
|
@@ -395,8 +603,18 @@ async function cmdVerify(args) {
|
|
|
395
603
|
evidence,
|
|
396
604
|
});
|
|
397
605
|
|
|
398
|
-
|
|
606
|
+
// `decided: "elsewhere"` and `panelUnavailable` are 200 responses that carry NO
|
|
607
|
+
// `verdict` key, so `data.verdict ?? "unknown"` fell to the final arm below and told
|
|
608
|
+
// the builder "❌ REJECTED — resubmit with stronger proof" for a milestone that may
|
|
609
|
+
// well have been approved and paid (SEC-055). Classified before the verdict is read.
|
|
610
|
+
const verdict = data.decided === "elsewhere"
|
|
611
|
+
? "decided_elsewhere"
|
|
612
|
+
: data.panelUnavailable
|
|
613
|
+
? "panel_unavailable"
|
|
614
|
+
: (data.verdict ?? "unknown");
|
|
399
615
|
const confidence = data.confidence ?? 0;
|
|
616
|
+
// Approved, payout SCHEDULED, funds still in escrow so the funder can contest.
|
|
617
|
+
const releaseHoldUntil = data.release_hold_until ?? null;
|
|
400
618
|
|
|
401
619
|
// Print model votes if available
|
|
402
620
|
const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
|
|
@@ -407,20 +625,43 @@ async function cmdVerify(args) {
|
|
|
407
625
|
process.stderr.write("\n");
|
|
408
626
|
|
|
409
627
|
let statusLine;
|
|
410
|
-
if (verdict === "
|
|
628
|
+
if (verdict === "decided_elsewhere") {
|
|
629
|
+
statusLine =
|
|
630
|
+
"ℹ️ This milestone was decided by another request while your proof was being verified — " +
|
|
631
|
+
"by the funder's review, or by a concurrent submission. Your evidence was recorded. " +
|
|
632
|
+
"Check the milestone status before resubmitting";
|
|
633
|
+
} else if (verdict === "panel_unavailable") {
|
|
634
|
+
statusLine =
|
|
635
|
+
"⏳ PENDING_REVIEW — the verification panel could not reach a verdict (too few models " +
|
|
636
|
+
"answered). This is NOT a rejection and NOT a judgement of your work; a human decides, " +
|
|
637
|
+
"and resubmitting the same evidence will not help while the panel is down";
|
|
638
|
+
} else if (verdict === "approved" && releaseHoldUntil) {
|
|
639
|
+
statusLine =
|
|
640
|
+
`⏸️ APPROVED (${confidence}% confidence) — payout scheduled for ${releaseHoldUntil}. ` +
|
|
641
|
+
"NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move";
|
|
642
|
+
} else if (verdict === "approved") {
|
|
411
643
|
statusLine = `✅ VERIFIED (${confidence}% confidence)`;
|
|
412
644
|
if (data.on_chain_url) statusLine += `\n Proof: ${data.on_chain_url}`;
|
|
413
645
|
} else if (verdict === "pending_review") {
|
|
414
646
|
statusLine = `⏳ PENDING_REVIEW (${confidence}% confidence) — manual review triggered`;
|
|
415
|
-
} else {
|
|
647
|
+
} else if (verdict === "rejected") {
|
|
416
648
|
statusLine = `❌ REJECTED (${confidence}% confidence) — resubmit with stronger proof`;
|
|
649
|
+
} else {
|
|
650
|
+
// Not a rejection: an outcome this version does not know. Drawing no conclusion is
|
|
651
|
+
// the one answer that cannot be wrong, and it makes the next value visible instead
|
|
652
|
+
// of silently a rejection.
|
|
653
|
+
statusLine =
|
|
654
|
+
`⚠️ This submission finished with an outcome this CLI does not recognise (verdict: ${verdict}). ` +
|
|
655
|
+
"No conclusion was drawn — check the milestone status and upgrade cascrow-cli";
|
|
417
656
|
}
|
|
418
657
|
process.stderr.write(statusLine + "\n\n");
|
|
419
658
|
|
|
420
659
|
print({
|
|
421
660
|
verdict,
|
|
422
661
|
confidence,
|
|
423
|
-
|
|
662
|
+
// MONEY MOVED — not "the verdict was approved". A scheduled payout has not happened.
|
|
663
|
+
passed: verdict === "approved" && !releaseHoldUntil,
|
|
664
|
+
release_hold_until: releaseHoldUntil,
|
|
424
665
|
reasoning: data.reasoning ?? "",
|
|
425
666
|
on_chain_url: data.on_chain_url ?? null,
|
|
426
667
|
proof_id: data.proof_id ?? null,
|
|
@@ -474,7 +715,14 @@ async function cmdEscrowFund(args) {
|
|
|
474
715
|
agentPrivateKey: args["private-key"],
|
|
475
716
|
};
|
|
476
717
|
if (args.milestone) body.milestoneId = args.milestone;
|
|
477
|
-
if (args.amount)
|
|
718
|
+
if (args.amount) {
|
|
719
|
+
// NOT forwarded. `agent/escrow-fund` funds `milestone.amountUSD` and ignores
|
|
720
|
+
// a body field, so `--amount` never overrode anything — it only told the
|
|
721
|
+
// user it had. Say so rather than sending a number the server drops.
|
|
722
|
+
process.stderr.write(
|
|
723
|
+
"\n⚠️ --amount is ignored for escrow funding: the escrowed amount is the milestone's own amountUSD.\n"
|
|
724
|
+
);
|
|
725
|
+
}
|
|
478
726
|
|
|
479
727
|
process.stderr.write("\nFunding milestone on-chain (approve + fundMilestone)...\n");
|
|
480
728
|
const data = await apiPost("/api/agent/escrow-fund", body);
|
|
@@ -540,6 +788,62 @@ async function cmdCheckInvites() {
|
|
|
540
788
|
}
|
|
541
789
|
}
|
|
542
790
|
|
|
791
|
+
/**
|
|
792
|
+
* The funder's pending-verdict queue, and the verdict itself.
|
|
793
|
+
*
|
|
794
|
+
* Both go through the routes every other surface uses — `GET
|
|
795
|
+
* /api/agent/review-queue` and `POST /api/contracts/review` — which ask the same
|
|
796
|
+
* owner module, so the queue this prints and the queue the door accepts a
|
|
797
|
+
* verdict on cannot come apart (F-22).
|
|
798
|
+
*
|
|
799
|
+
* `review-queue` is the pull behind the `manual_review.required` webhook: a
|
|
800
|
+
* webhook nobody received is a decision nobody makes, and cascrow auto-approves
|
|
801
|
+
* an undecided milestone after 48 h (agent contracts) or 14 days (human) and
|
|
802
|
+
* releases the escrow. `review` ships with it rather than later, because a list
|
|
803
|
+
* its reader cannot act on names a remedy they are unable to perform (F-72).
|
|
804
|
+
*/
|
|
805
|
+
async function cmdReviewQueue() {
|
|
806
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
807
|
+
const data = await apiGet("/api/agent/review-queue");
|
|
808
|
+
const contracts = data.contracts ?? [];
|
|
809
|
+
print(data);
|
|
810
|
+
if (contracts.length === 0) {
|
|
811
|
+
process.stderr.write("\nNo milestones are waiting for your verdict.\n\n");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const lines = contracts.map((c) => {
|
|
815
|
+
const pending = c.pendingMilestones ?? [];
|
|
816
|
+
const ms =
|
|
817
|
+
pending.length === 1
|
|
818
|
+
? `milestone ${pending[0].milestoneId}`
|
|
819
|
+
: pending.length > 1
|
|
820
|
+
? `${pending.length} milestones pending — pass --milestone <id>`
|
|
821
|
+
: "contract-level review — omit --milestone";
|
|
822
|
+
return ` ${c.contractId} ${c.amountUSD} RLUSD (${ms})`;
|
|
823
|
+
}).join("\n");
|
|
824
|
+
const more = data.truncated ? `\n(${data.total} in total — this list is capped.)` : "";
|
|
825
|
+
process.stderr.write(
|
|
826
|
+
`\n${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\n` +
|
|
827
|
+
"Decide with: cascrow review --contract <id> --decision APPROVE|REJECT [--milestone <id>]\n\n"
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function cmdReview(args) {
|
|
832
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
833
|
+
const contractId = args.contract;
|
|
834
|
+
const decision = (args.decision ?? "").toUpperCase();
|
|
835
|
+
if (!contractId) err("--contract <id> is required (see: cascrow review-queue)");
|
|
836
|
+
if (decision !== "APPROVE" && decision !== "REJECT") err("--decision must be APPROVE or REJECT");
|
|
837
|
+
const body = args.milestone ? { contractId, decision, milestoneId: args.milestone } : { contractId, decision };
|
|
838
|
+
const data = await apiPost("/api/contracts/review", body);
|
|
839
|
+
print(data);
|
|
840
|
+
const what =
|
|
841
|
+
decision === "APPROVE"
|
|
842
|
+
? "approved — the escrow is being released to the builder"
|
|
843
|
+
: "rejected — the milestone is back with the builder, who can resubmit";
|
|
844
|
+
process.stderr.write(`\nMilestone ${what}.${data.txHash ? `\nRelease tx: ${data.txHash}` : ""}\n\n`);
|
|
845
|
+
}
|
|
846
|
+
|
|
543
847
|
async function cmdMe() {
|
|
544
848
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
545
849
|
const data = await apiGet("/api/agent/me");
|
|
@@ -569,6 +873,8 @@ try {
|
|
|
569
873
|
case "join": await cmdJoin(args); break;
|
|
570
874
|
case "handoff": await cmdHandoff(args); break;
|
|
571
875
|
case "check-invites": await cmdCheckInvites(); break;
|
|
876
|
+
case "review-queue": await cmdReviewQueue(); break;
|
|
877
|
+
case "review": await cmdReview(args); break;
|
|
572
878
|
case "me": await cmdMe(); break;
|
|
573
879
|
default:
|
|
574
880
|
console.error(`Unknown command: ${command}\n`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cascrow-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for Cascrow —
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "CLI for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and milestone escrow on the XRP Ledger",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"milestone",
|
|
21
21
|
"agent",
|
|
22
22
|
"agentic",
|
|
23
|
-
"verification"
|
|
23
|
+
"verification",
|
|
24
|
+
"reputation",
|
|
25
|
+
"trust",
|
|
26
|
+
"agent-economy"
|
|
24
27
|
],
|
|
25
28
|
"author": "Cascrow <dev@cascrow.com>",
|
|
26
29
|
"license": "MIT",
|