cascrow-cli 1.2.2 → 1.4.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 +9 -0
- package/README.md +50 -6
- package/builder-agent.js +236 -46
- package/index.js +636 -55
- package/package.json +6 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.4.0 — 2026-09-25
|
|
4
|
+
|
|
5
|
+
- New `cascrow status --proof <id> [--wait]` — show, or wait for, a proof's verification result. `submit` now points to it: verification starts by itself after an upload.
|
|
6
|
+
- `cascrow verify --proof <id>` on a proof whose verification is already running waits for that run instead of failing.
|
|
7
|
+
- `cascrow create` takes `--criteria`, `--criteria-file`, `--checklist-file` and `--review-mode` — the bar the AI panel judges against, kept out of the title (which is printed on the certificate).
|
|
8
|
+
- Every API call has a deadline and survives a non-JSON answer; errors end with the API's stable code in brackets, e.g. `[RATE_LIMITED]`.
|
|
9
|
+
- `status` never reports a released milestone as closed.
|
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
|
|
|
@@ -50,11 +53,14 @@ contract, no escrow, no wallet.
|
|
|
50
53
|
| `create` | Create a milestone contract | Yes |
|
|
51
54
|
| `fund` | Activate a milestone (simulation — no on-chain tx) | Yes |
|
|
52
55
|
| `escrow-fund` | Fund a milestone with real on-chain RLUSD escrow | Yes |
|
|
53
|
-
| `submit` | Upload proof text
|
|
56
|
+
| `submit` | Upload proof text — verification starts by itself; returns `proofId` | Yes |
|
|
57
|
+
| `status` | Show, or `--wait` for, the verification result of a proof | Yes |
|
|
54
58
|
| `get` | Get contract status and milestones | Yes |
|
|
55
59
|
| `join` | Join a contract as Builder via invite code | Yes |
|
|
56
60
|
| `handoff` | Send a contract invite to a Builder agent by Agent ID | Yes |
|
|
57
61
|
| `check-invites` | Check pending contract invites for this agent | Yes |
|
|
62
|
+
| `review-queue` | As funder: list milestones still waiting for your verdict | Yes |
|
|
63
|
+
| `review` | As funder: approve or reject a milestone under manual review | Yes |
|
|
58
64
|
| `me` | Print this agent's ID | Yes |
|
|
59
65
|
|
|
60
66
|
Run `cascrow --help` (or `cascrow`) any time for the full, always-current option reference.
|
|
@@ -92,11 +98,18 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
|
|
|
92
98
|
### `create`
|
|
93
99
|
|
|
94
100
|
```
|
|
95
|
-
--title <title> required milestone title
|
|
101
|
+
--title <title> required short milestone title (printed on the certificate)
|
|
96
102
|
--days <n> optional deadline days from now (default: 7)
|
|
97
103
|
--amount <usd> optional RLUSD to lock in escrow (default: 0 = verification-only)
|
|
104
|
+
--criteria <text> optional what the AI panel checks the proof against (max 4000 chars)
|
|
105
|
+
--criteria-file <path> optional the same, read from a file
|
|
106
|
+
--checklist-file <path> optional JSON array of { title, severity?, id? } (max 20), scored item by item on text proofs
|
|
107
|
+
--review-mode <mode> optional AUTO | MANUAL | MANUAL_AUTO — what happens when the panel is unsure (60–85%)
|
|
98
108
|
```
|
|
99
109
|
|
|
110
|
+
Keep acceptance criteria out of `--title`: the title is printed on the public
|
|
111
|
+
certificate, and the panel judges the proof against `--criteria`.
|
|
112
|
+
|
|
100
113
|
### `fund`
|
|
101
114
|
|
|
102
115
|
```
|
|
@@ -109,11 +122,11 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
|
|
|
109
122
|
```
|
|
110
123
|
--contract <id> required
|
|
111
124
|
--private-key <key> required EVM private key (funds come from this wallet)
|
|
112
|
-
--amount <usd>
|
|
125
|
+
--amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
|
|
113
126
|
--milestone <id> optional
|
|
114
127
|
```
|
|
115
128
|
|
|
116
|
-
### `submit` [
|
|
129
|
+
### `submit` [or use `verify` for text proof in one call]
|
|
117
130
|
|
|
118
131
|
```
|
|
119
132
|
--milestone <id> required
|
|
@@ -121,6 +134,19 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
|
|
|
121
134
|
--file <name> optional filename (default: proof-report.txt)
|
|
122
135
|
```
|
|
123
136
|
|
|
137
|
+
Verification starts by itself after the upload — wait for it with `status`, don't
|
|
138
|
+
start it again with `verify --proof` (that would only wait for the same run).
|
|
139
|
+
|
|
140
|
+
### `status`
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
--proof <id> required
|
|
144
|
+
--wait optional wait up to ~2 min; stops early on 401/403/404 or an interrupted run
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Errors end with the API's stable code in brackets, e.g. `[RATE_LIMITED]` — branch
|
|
148
|
+
on that, not on the wording.
|
|
149
|
+
|
|
124
150
|
### `get`
|
|
125
151
|
|
|
126
152
|
```
|
|
@@ -146,6 +172,24 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
|
|
|
146
172
|
|
|
147
173
|
No options.
|
|
148
174
|
|
|
175
|
+
### `review-queue`
|
|
176
|
+
|
|
177
|
+
No options. Lists the contracts on which you, as funder, still owe a manual
|
|
178
|
+
verdict — the pull behind the `manual_review.required` webhook. A webhook nobody
|
|
179
|
+
received is a decision nobody makes, and cascrow auto-approves an undecided
|
|
180
|
+
milestone after 48 hours (agent contracts) or 14 days (human) and releases the
|
|
181
|
+
escrow, so poll this whenever you have funded work in flight.
|
|
182
|
+
|
|
183
|
+
### `review`
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
--contract <id> required from `cascrow review-queue`
|
|
187
|
+
--decision <APPROVE|REJECT> required APPROVE releases the escrow; REJECT returns
|
|
188
|
+
the milestone to the builder, who can resubmit
|
|
189
|
+
--milestone <id> required once more than one milestone of the contract is
|
|
190
|
+
pending review; `review-queue` names it
|
|
191
|
+
```
|
|
192
|
+
|
|
149
193
|
## Environment
|
|
150
194
|
|
|
151
195
|
| Variable | Description |
|
package/builder-agent.js
CHANGED
|
@@ -15,24 +15,57 @@ if (!API_KEY) {
|
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|
|
26
60
|
return data;
|
|
27
61
|
}
|
|
28
62
|
|
|
63
|
+
async function apiPost(path, body) {
|
|
64
|
+
return fetchJson("POST", path, { json: body });
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
async function apiGet(path) {
|
|
30
|
-
|
|
31
|
-
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
32
|
-
});
|
|
33
|
-
const data = await res.json();
|
|
34
|
-
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
35
|
-
return data;
|
|
68
|
+
return fetchJson("GET", path);
|
|
36
69
|
}
|
|
37
70
|
|
|
38
71
|
async function uploadProof(milestoneId, content) {
|
|
@@ -40,35 +73,73 @@ async function uploadProof(milestoneId, content) {
|
|
|
40
73
|
const form = new FormData();
|
|
41
74
|
form.append("file", blob, "proof-report.txt");
|
|
42
75
|
form.append("milestoneId", milestoneId);
|
|
43
|
-
|
|
44
|
-
method: "POST",
|
|
45
|
-
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
46
|
-
body: form,
|
|
47
|
-
});
|
|
48
|
-
const data = await res.json();
|
|
49
|
-
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
50
|
-
return data;
|
|
76
|
+
return fetchJson("POST", "/api/proof/upload", { form, deadlineMs: UPLOAD_CLIENT_ABORT_MS });
|
|
51
77
|
}
|
|
52
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`.
|
|
53
91
|
async function streamVerify(proofId) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
63
120
|
if (!res.ok) {
|
|
64
|
-
|
|
65
|
-
|
|
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");
|
|
66
136
|
}
|
|
67
137
|
|
|
68
138
|
const reader = res.body.getReader();
|
|
69
139
|
const decoder = new TextDecoder();
|
|
70
140
|
let buffer = "";
|
|
71
141
|
|
|
142
|
+
try {
|
|
72
143
|
while (true) {
|
|
73
144
|
const { done, value } = await reader.read();
|
|
74
145
|
if (done) break;
|
|
@@ -95,6 +166,21 @@ async function streamVerify(proofId) {
|
|
|
95
166
|
}
|
|
96
167
|
}
|
|
97
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
|
+
}
|
|
98
184
|
throw new Error("Stream ended without complete event");
|
|
99
185
|
}
|
|
100
186
|
|
|
@@ -172,28 +258,115 @@ VERIFICATION CRITERIA MET:
|
|
|
172
258
|
// Stream disconnected before complete event (e.g. Vercel 150s timeout).
|
|
173
259
|
// The verification is still running server-side — just poll for the result,
|
|
174
260
|
// do NOT call streamVerify again (would trigger duplicate AI verification).
|
|
175
|
-
|
|
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"];
|
|
176
291
|
for (let i = 0; i < 60; i++) {
|
|
177
292
|
await new Promise((r) => setTimeout(r, 5000));
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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}`);
|
|
183
315
|
break;
|
|
184
316
|
}
|
|
185
317
|
}
|
|
186
318
|
if (!result) throw new Error("Timed out waiting for verification result");
|
|
187
319
|
}
|
|
188
320
|
|
|
189
|
-
|
|
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;
|
|
190
333
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
191
|
-
if (
|
|
192
|
-
|
|
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.`);
|
|
193
366
|
} else {
|
|
194
|
-
console.log(
|
|
367
|
+
console.log(`⚠️ Outcome not recognised by this version (action: ${result.action}). No conclusion drawn.`);
|
|
195
368
|
}
|
|
196
|
-
console.log(` Decision: ${result.decision}`);
|
|
369
|
+
console.log(` Decision: ${result.decision ?? "none read (the stream dropped before it)"}`);
|
|
197
370
|
console.log(` View contract: ${BASE_URL}/contract/${invite.contractId}`);
|
|
198
371
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
199
372
|
}
|
|
@@ -210,13 +383,30 @@ async function main() {
|
|
|
210
383
|
console.log(` Share this ID with the Requester agent`);
|
|
211
384
|
console.log(` Polling for work every ${POLL_INTERVAL_MS / 1000}s...\n`);
|
|
212
385
|
|
|
213
|
-
// Register EVM wallet address so Requester can fund real escrow to this agent
|
|
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.
|
|
214
396
|
if (AGENT_EVM_WALLET) {
|
|
215
397
|
try {
|
|
216
398
|
await apiPost("/api/user/wallet", { walletAddress: AGENT_EVM_WALLET });
|
|
217
399
|
console.log(` 💳 EVM wallet registered: ${AGENT_EVM_WALLET}\n`);
|
|
218
400
|
} catch (e) {
|
|
219
|
-
console.warn(
|
|
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
|
+
);
|
|
220
410
|
}
|
|
221
411
|
}
|
|
222
412
|
|