clawlabor 1.11.1
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/CONTRIBUTING.md +62 -0
- package/COPYRIGHT +41 -0
- package/LICENSE +661 -0
- package/QUICKSTART.md +154 -0
- package/README.md +283 -0
- package/REFERENCE.md +821 -0
- package/SECURITY.md +77 -0
- package/SKILL.md +470 -0
- package/WORKFLOW.md +273 -0
- package/bin/clawlabor.js +29 -0
- package/bin/install.js +264 -0
- package/examples/buyer-workflow.md +69 -0
- package/examples/provider-workflow.md +98 -0
- package/package.json +49 -0
- package/runtime/cli.js +434 -0
- package/runtime/commands/command-accept.js +59 -0
- package/runtime/commands/command-api-base.js +11 -0
- package/runtime/commands/command-auth.js +36 -0
- package/runtime/commands/command-bootstrap.js +25 -0
- package/runtime/commands/command-buy.js +75 -0
- package/runtime/commands/command-cancel.js +66 -0
- package/runtime/commands/command-complete.js +69 -0
- package/runtime/commands/command-confirm.js +51 -0
- package/runtime/commands/command-credentials-path.js +50 -0
- package/runtime/commands/command-delete-attachment.js +9 -0
- package/runtime/commands/command-doctor.js +125 -0
- package/runtime/commands/command-inspect.js +68 -0
- package/runtime/commands/command-list-attachments.js +50 -0
- package/runtime/commands/command-match.js +52 -0
- package/runtime/commands/command-me.js +50 -0
- package/runtime/commands/command-message.js +78 -0
- package/runtime/commands/command-orders.js +94 -0
- package/runtime/commands/command-plan.js +165 -0
- package/runtime/commands/command-post.js +83 -0
- package/runtime/commands/command-profile.js +78 -0
- package/runtime/commands/command-publish.js +80 -0
- package/runtime/commands/command-register.js +84 -0
- package/runtime/commands/command-result.js +69 -0
- package/runtime/commands/command-solve.js +467 -0
- package/runtime/commands/command-stage.js +56 -0
- package/runtime/commands/command-status.js +147 -0
- package/runtime/commands/command-upload-attachment.js +55 -0
- package/runtime/commands/command-validate.js +51 -0
- package/runtime/commands/command-wait.js +62 -0
- package/runtime/commands/core.js +67 -0
- package/runtime/commands/runtime.js +756 -0
- package/runtime/commands/shared.js +660 -0
- package/runtime/http.js +215 -0
- package/runtime/options.js +36 -0
- package/runtime/session.js +369 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
ApiError,
|
|
7
|
+
apiBase,
|
|
8
|
+
credentialState,
|
|
9
|
+
credentialsFilePath,
|
|
10
|
+
makeIdempotencyKey,
|
|
11
|
+
makePublishIdempotencyKey,
|
|
12
|
+
request,
|
|
13
|
+
requestJson,
|
|
14
|
+
requestJsonNoAuth,
|
|
15
|
+
requestMultipart,
|
|
16
|
+
resolveApiKey,
|
|
17
|
+
writeCredentialsFile,
|
|
18
|
+
} = require("../http");
|
|
19
|
+
const {
|
|
20
|
+
numberOption,
|
|
21
|
+
positiveNumberOption,
|
|
22
|
+
requiredOption,
|
|
23
|
+
} = require("../options");
|
|
24
|
+
|
|
25
|
+
const TERMINAL_ORDER_STATES = new Set([
|
|
26
|
+
"pending_confirmation",
|
|
27
|
+
"completed",
|
|
28
|
+
"cancelled",
|
|
29
|
+
"in_dispute",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function readAttachmentOptions(options, fileOptionName = "file") {
|
|
33
|
+
const filePath = ensureUploadPathAllowed(requiredOption(options, fileOptionName));
|
|
34
|
+
return {
|
|
35
|
+
filePath,
|
|
36
|
+
filename: options.filename || path.basename(filePath),
|
|
37
|
+
contentType: options["content-type"] || "application/octet-stream",
|
|
38
|
+
description: options.description,
|
|
39
|
+
overwriteFilename: options["overwrite-filename"],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function uploadAttachment(deps, entity, id, attachment) {
|
|
44
|
+
const bytes = fs.readFileSync(attachment.filePath);
|
|
45
|
+
const formData = new FormData();
|
|
46
|
+
formData.append(
|
|
47
|
+
"file",
|
|
48
|
+
new Blob([bytes], { type: attachment.contentType }),
|
|
49
|
+
attachment.filename,
|
|
50
|
+
);
|
|
51
|
+
if (attachment.description) formData.append("description", attachment.description);
|
|
52
|
+
if (attachment.overwriteFilename) {
|
|
53
|
+
formData.append("overwrite_filename", attachment.overwriteFilename);
|
|
54
|
+
}
|
|
55
|
+
return requestMultipart(
|
|
56
|
+
deps,
|
|
57
|
+
"POST",
|
|
58
|
+
attachmentPath({ entity, id }),
|
|
59
|
+
formData,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// policy / requirement helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function parseRequirement(options) {
|
|
68
|
+
if (options["requirement-json"]) {
|
|
69
|
+
return JSON.parse(options["requirement-json"]);
|
|
70
|
+
}
|
|
71
|
+
if (options["requirement-file"]) {
|
|
72
|
+
return JSON.parse(fs.readFileSync(options["requirement-file"], "utf8"));
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseJsonOption(options, jsonName, fileName, fallback = undefined) {
|
|
78
|
+
if (options[jsonName]) {
|
|
79
|
+
return JSON.parse(options[jsonName]);
|
|
80
|
+
}
|
|
81
|
+
if (options[fileName]) {
|
|
82
|
+
return JSON.parse(fs.readFileSync(options[fileName], "utf8"));
|
|
83
|
+
}
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stringOptionFromFile(options, valueName, fileName, fallback = undefined) {
|
|
88
|
+
if (options[valueName] !== undefined) return options[valueName];
|
|
89
|
+
if (options[fileName]) return fs.readFileSync(options[fileName], "utf8");
|
|
90
|
+
return fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// attachment staging helpers
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
const URL_FIELD_SUFFIXES = ["_url", "_uri"];
|
|
98
|
+
const BLOCKED_EXTENSIONS = new Set([".exe", ".bat", ".sh", ".dll", ".ps1", ".cmd", ".vbs", ".js"]);
|
|
99
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
100
|
+
|
|
101
|
+
// Hard blocklist for upload paths: protects against prompt-injected agents
|
|
102
|
+
// being tricked into exfiltrating local secrets. Users extend it via
|
|
103
|
+
// CLAWLABOR_UPLOAD_BLOCKLIST (colon-separated absolute paths).
|
|
104
|
+
const SENSITIVE_HOME_PREFIXES = [
|
|
105
|
+
".ssh",
|
|
106
|
+
".aws",
|
|
107
|
+
".gnupg",
|
|
108
|
+
".kube",
|
|
109
|
+
".docker/config.json",
|
|
110
|
+
".netrc",
|
|
111
|
+
".npmrc",
|
|
112
|
+
".pypirc",
|
|
113
|
+
".config/clawlabor",
|
|
114
|
+
".config/gcloud",
|
|
115
|
+
".config/gh",
|
|
116
|
+
".config/op",
|
|
117
|
+
".config/anthropic",
|
|
118
|
+
".claude",
|
|
119
|
+
".codex",
|
|
120
|
+
".openclaw",
|
|
121
|
+
".hermes",
|
|
122
|
+
];
|
|
123
|
+
const SENSITIVE_BASENAME_PATTERNS = [
|
|
124
|
+
/^\.env(\..+)?$/i,
|
|
125
|
+
/(^|[._-])credentials?($|[._-])/i,
|
|
126
|
+
/(^|[._-])secrets?($|[._-])/i,
|
|
127
|
+
/^id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/i,
|
|
128
|
+
/\.pem$/i,
|
|
129
|
+
/\.pfx$/i,
|
|
130
|
+
/\.p12$/i,
|
|
131
|
+
/\.key$/i,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
function expandUser(p) {
|
|
135
|
+
if (!p) return p;
|
|
136
|
+
if (p === "~") return os.homedir();
|
|
137
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
138
|
+
return p;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ensureUploadPathAllowed(localPath, env = process.env) {
|
|
142
|
+
if (!localPath) {
|
|
143
|
+
throw new Error("Upload path is required");
|
|
144
|
+
}
|
|
145
|
+
const resolved = path.resolve(expandUser(localPath));
|
|
146
|
+
let realPath = resolved;
|
|
147
|
+
try {
|
|
148
|
+
realPath = fs.realpathSync(resolved);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err.code !== "ENOENT") throw err;
|
|
151
|
+
// Path does not exist yet; later fs.statSync will surface the error.
|
|
152
|
+
}
|
|
153
|
+
const home = os.homedir();
|
|
154
|
+
const extraRaw = (env.CLAWLABOR_UPLOAD_BLOCKLIST || "")
|
|
155
|
+
.split(":")
|
|
156
|
+
.map((s) => s.trim())
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.map((rawEntry) => path.resolve(expandUser(rawEntry)));
|
|
159
|
+
|
|
160
|
+
const candidates = resolved === realPath ? [resolved] : [resolved, realPath];
|
|
161
|
+
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
const base = path.basename(candidate);
|
|
164
|
+
for (const pattern of SENSITIVE_BASENAME_PATTERNS) {
|
|
165
|
+
if (pattern.test(base)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Refusing to upload sensitive file: ${candidate} (basename matches ${pattern}). ` +
|
|
168
|
+
"If this is a deliberate user-authorized upload, copy/rename the file to a non-sensitive path first.",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const rel of SENSITIVE_HOME_PREFIXES) {
|
|
173
|
+
const blocked = path.join(home, rel);
|
|
174
|
+
if (candidate === blocked || candidate.startsWith(`${blocked}${path.sep}`)) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Refusing to upload from protected location: ${candidate} (under ${blocked}). ` +
|
|
177
|
+
"Move the file outside this directory before uploading.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const entry of extraRaw) {
|
|
182
|
+
if (candidate === entry || candidate.startsWith(`${entry}${path.sep}`)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Refusing to upload: ${candidate} matches CLAWLABOR_UPLOAD_BLOCKLIST entry ${entry}.`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return realPath;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hasUriSchemaField(fieldName, inputSchema) {
|
|
194
|
+
return inputSchema?.properties?.[fieldName]?.format === "uri";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isStrictUrlField(fieldName, inputSchema) {
|
|
198
|
+
const name = fieldName.toLowerCase();
|
|
199
|
+
if (URL_FIELD_SUFFIXES.some((s) => name.endsWith(s))) return true;
|
|
200
|
+
if (hasUriSchemaField(fieldName, inputSchema)) return true;
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isUrlField(fieldName, inputSchema) {
|
|
205
|
+
return isStrictUrlField(fieldName, inputSchema);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseInputFlags(inputValues) {
|
|
209
|
+
return (inputValues || []).map((raw) => {
|
|
210
|
+
const eqIdx = raw.indexOf("=");
|
|
211
|
+
if (eqIdx === -1) throw new Error(`--input must be in field=value format, got: ${raw}`);
|
|
212
|
+
const field = raw.slice(0, eqIdx);
|
|
213
|
+
const val = raw.slice(eqIdx + 1);
|
|
214
|
+
return { field, isFile: false, value: val };
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseFileFlags(fileValues) {
|
|
219
|
+
return (fileValues || []).map((raw) => {
|
|
220
|
+
const eqIdx = raw.indexOf("=");
|
|
221
|
+
if (eqIdx === -1) throw new Error(`--file must be in field=path format, got: ${raw}`);
|
|
222
|
+
const field = raw.slice(0, eqIdx);
|
|
223
|
+
const localPath = raw.slice(eqIdx + 1);
|
|
224
|
+
if (!field || !localPath) throw new Error(`--file must be in field=path format, got: ${raw}`);
|
|
225
|
+
return { field, isFile: true, localPath, source: "file" };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function guessMimeType(ext) {
|
|
230
|
+
const map = {
|
|
231
|
+
".html": "text/html",
|
|
232
|
+
".htm": "text/html",
|
|
233
|
+
".svg": "image/svg+xml",
|
|
234
|
+
".png": "image/png",
|
|
235
|
+
".jpg": "image/jpeg",
|
|
236
|
+
".jpeg": "image/jpeg",
|
|
237
|
+
".pdf": "application/pdf",
|
|
238
|
+
".zip": "application/zip",
|
|
239
|
+
".css": "text/css",
|
|
240
|
+
".txt": "text/plain",
|
|
241
|
+
};
|
|
242
|
+
return map[ext] || "application/octet-stream";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function stageAndUploadFile(deps, entry) {
|
|
246
|
+
const { field } = entry;
|
|
247
|
+
const localPath = ensureUploadPathAllowed(entry.localPath, deps.env);
|
|
248
|
+
const base = apiBase(deps.env);
|
|
249
|
+
const apiKey = resolveApiKey(deps.env);
|
|
250
|
+
|
|
251
|
+
// Fast-fail checks
|
|
252
|
+
const stat = fs.statSync(localPath);
|
|
253
|
+
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
254
|
+
throw new Error(`File too large: ${stat.size} bytes (max ${MAX_UPLOAD_BYTES})`);
|
|
255
|
+
}
|
|
256
|
+
const ext = path.extname(localPath).toLowerCase();
|
|
257
|
+
if (BLOCKED_EXTENSIONS.has(ext)) {
|
|
258
|
+
throw new Error(`Blocked file extension: ${ext}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const filename = path.basename(localPath);
|
|
262
|
+
const bytes = fs.readFileSync(localPath);
|
|
263
|
+
const sha256 = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
264
|
+
const mimeType = guessMimeType(ext);
|
|
265
|
+
|
|
266
|
+
if (mimeType === "text/html" || mimeType === "image/svg+xml") {
|
|
267
|
+
process.stderr.write(
|
|
268
|
+
`Warning: ${filename} is a high-risk input (HTML/SVG). Seller must render in a sandboxed browser with no network access.\n`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 1. Stage
|
|
273
|
+
const stageResp = await deps.fetch(`${base}/attachments/stage`, {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: {
|
|
276
|
+
Authorization: `Bearer ${apiKey}`,
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
original_filename: filename,
|
|
281
|
+
requirement_field: field,
|
|
282
|
+
mime_type: mimeType,
|
|
283
|
+
size_bytes: stat.size,
|
|
284
|
+
sha256,
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
if (!stageResp.ok) {
|
|
288
|
+
const body = await stageResp.text();
|
|
289
|
+
throw new Error(`Stage failed (${stageResp.status}): ${body}`);
|
|
290
|
+
}
|
|
291
|
+
const staged = JSON.parse(await stageResp.text());
|
|
292
|
+
|
|
293
|
+
// 2. PUT to presigned upload URL
|
|
294
|
+
const putResp = await deps.fetch(staged.upload_url, {
|
|
295
|
+
method: "PUT",
|
|
296
|
+
headers: { "Content-Type": mimeType },
|
|
297
|
+
body: bytes,
|
|
298
|
+
});
|
|
299
|
+
if (!putResp.ok) {
|
|
300
|
+
throw new Error(`S3 PUT failed (${putResp.status})`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 3. Confirm
|
|
304
|
+
const confirmResp = await deps.fetch(
|
|
305
|
+
`${base}/attachments/stage/${staged.staged_attachment_id}/confirm`,
|
|
306
|
+
{
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: {
|
|
309
|
+
Authorization: `Bearer ${apiKey}`,
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
},
|
|
312
|
+
body: JSON.stringify({ sha256 }),
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
if (!confirmResp.ok) {
|
|
316
|
+
const body = await confirmResp.text();
|
|
317
|
+
throw new Error(`Confirm failed (${confirmResp.status}): ${body}`);
|
|
318
|
+
}
|
|
319
|
+
const confirmed = JSON.parse(await confirmResp.text());
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
field,
|
|
323
|
+
stagedId: staged.staged_attachment_id,
|
|
324
|
+
signedUrl: confirmed.signed_download_url,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function attachmentPath(options, includeFileId = false) {
|
|
329
|
+
const entity = requiredOption(options, "entity");
|
|
330
|
+
const id = requiredOption(options, "id");
|
|
331
|
+
const fileId = includeFileId ? requiredOption(options, "file-id") : null;
|
|
332
|
+
const prefixes = {
|
|
333
|
+
order: "orders",
|
|
334
|
+
orders: "orders",
|
|
335
|
+
task: "tasks",
|
|
336
|
+
tasks: "tasks",
|
|
337
|
+
submission: "task-submissions",
|
|
338
|
+
"task-submission": "task-submissions",
|
|
339
|
+
"task-submissions": "task-submissions",
|
|
340
|
+
};
|
|
341
|
+
const prefix = prefixes[entity];
|
|
342
|
+
if (!prefix) {
|
|
343
|
+
throw new Error("--entity must be one of: order, task, submission");
|
|
344
|
+
}
|
|
345
|
+
return `/${prefix}/${id}/attachments${fileId ? `/${fileId}` : ""}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function loadPolicy(options, env) {
|
|
349
|
+
const policyFile = options["policy-file"] || env.CLAWLABOR_POLICY_FILE;
|
|
350
|
+
if (!policyFile) {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
return JSON.parse(fs.readFileSync(policyFile, "utf8"));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function matchBody(options, flags, env, { defaultLimit } = {}) {
|
|
357
|
+
const policy = loadPolicy(options, env);
|
|
358
|
+
const body = {
|
|
359
|
+
goal: requiredOption(options, "goal"),
|
|
360
|
+
};
|
|
361
|
+
if (options.category) {
|
|
362
|
+
body.category = options.category;
|
|
363
|
+
} else if (Array.isArray(policy.allowed_categories) && policy.allowed_categories.length === 1) {
|
|
364
|
+
body.category = policy.allowed_categories[0];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const maxPrice = positiveNumberOption(options, "max-price");
|
|
368
|
+
if (maxPrice !== undefined) {
|
|
369
|
+
body.max_price = maxPrice;
|
|
370
|
+
} else if (policy.per_order_limit_uat !== undefined) {
|
|
371
|
+
body.max_price = Number(policy.per_order_limit_uat);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const minTrustScore = numberOption(options, "min-trust-score");
|
|
375
|
+
if (minTrustScore !== undefined) {
|
|
376
|
+
body.min_trust_score = minTrustScore;
|
|
377
|
+
} else if (policy.min_trust_score !== undefined) {
|
|
378
|
+
body.min_trust_score = Number(policy.min_trust_score);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const explicitLimit = numberOption(options, "limit");
|
|
382
|
+
if (explicitLimit !== undefined) {
|
|
383
|
+
body.limit = explicitLimit;
|
|
384
|
+
} else if (defaultLimit !== undefined) {
|
|
385
|
+
body.limit = defaultLimit;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const requireSchema = flags && flags.has("require-schema");
|
|
389
|
+
if (requireSchema) {
|
|
390
|
+
body.require_schema = true;
|
|
391
|
+
} else if (policy.require_schema === true) {
|
|
392
|
+
body.require_schema = true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const maxCompletionSeconds = positiveNumberOption(options, "max-completion-seconds");
|
|
396
|
+
if (maxCompletionSeconds !== undefined) {
|
|
397
|
+
body.max_completion_seconds = maxCompletionSeconds;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return body;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function validateRequirementAgainstSchema(requirement, schema) {
|
|
404
|
+
if (!schema || typeof schema !== "object") {
|
|
405
|
+
return { valid: true, missing: [] };
|
|
406
|
+
}
|
|
407
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
408
|
+
const missing = required.filter((key) => {
|
|
409
|
+
const value = requirement ? requirement[key] : undefined;
|
|
410
|
+
return value === undefined || value === null || value === "";
|
|
411
|
+
});
|
|
412
|
+
return { valid: missing.length === 0, missing };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function describeRequiredFields(schema) {
|
|
416
|
+
if (!schema || typeof schema !== "object" || !schema.properties) return [];
|
|
417
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
418
|
+
return required.map((name) => {
|
|
419
|
+
const prop = schema.properties[name] || {};
|
|
420
|
+
const exampleFromProp = prop.example !== undefined
|
|
421
|
+
? prop.example
|
|
422
|
+
: (Array.isArray(prop.examples) && prop.examples.length > 0 ? prop.examples[0] : null);
|
|
423
|
+
return {
|
|
424
|
+
name,
|
|
425
|
+
type: prop.type || "string",
|
|
426
|
+
format: prop.format || null,
|
|
427
|
+
description: prop.description || null,
|
|
428
|
+
enum: Array.isArray(prop.enum) ? prop.enum : null,
|
|
429
|
+
default: prop.default !== undefined ? prop.default : null,
|
|
430
|
+
example: exampleFromProp,
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function buildSampleRequirement(schema, providedRequirement) {
|
|
436
|
+
const sample = providedRequirement ? { ...providedRequirement } : {};
|
|
437
|
+
if (!schema || typeof schema !== "object" || !schema.properties) return sample;
|
|
438
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
439
|
+
for (const name of required) {
|
|
440
|
+
const current = sample[name];
|
|
441
|
+
if (current !== undefined && current !== null && current !== "") continue;
|
|
442
|
+
const prop = schema.properties[name] || {};
|
|
443
|
+
if (prop.example !== undefined) {
|
|
444
|
+
sample[name] = prop.example;
|
|
445
|
+
} else if (Array.isArray(prop.examples) && prop.examples.length > 0) {
|
|
446
|
+
sample[name] = prop.examples[0];
|
|
447
|
+
} else if (prop.default !== undefined) {
|
|
448
|
+
sample[name] = prop.default;
|
|
449
|
+
} else if (Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
450
|
+
sample[name] = prop.enum[0];
|
|
451
|
+
} else {
|
|
452
|
+
const typeTag = prop.type || "string";
|
|
453
|
+
const formatTag = prop.format ? `:${prop.format}` : "";
|
|
454
|
+
sample[name] = `<TODO:${name}:${typeTag}${formatTag}>`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return sample;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function shellQuote(value) {
|
|
461
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function pickCompatibleListing(matches, requirement) {
|
|
465
|
+
const allowed = matches.filter((item) => item.policy?.allowed !== false);
|
|
466
|
+
if (allowed.length === 0) return null;
|
|
467
|
+
|
|
468
|
+
if (!requirement || Object.keys(requirement).length === 0) {
|
|
469
|
+
return allowed[0];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const compatible = allowed.find((item) => {
|
|
473
|
+
const check = validateRequirementAgainstSchema(requirement, item.input_schema);
|
|
474
|
+
return check.valid;
|
|
475
|
+
});
|
|
476
|
+
return compatible || allowed[0];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function compactListingForPlan(listing) {
|
|
480
|
+
return {
|
|
481
|
+
id: listing?.id || null,
|
|
482
|
+
title: listing?.title || listing?.name || null,
|
|
483
|
+
price: listing?.price ?? null,
|
|
484
|
+
category: listing?.category || null,
|
|
485
|
+
trust_score: listing?.trust_score ?? null,
|
|
486
|
+
status: listing?.status || null,
|
|
487
|
+
inventory: listing?.inventory ?? null,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function summarizeOutputSchema(schema) {
|
|
492
|
+
if (!schema || typeof schema !== "object") return null;
|
|
493
|
+
const props = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
|
|
494
|
+
return {
|
|
495
|
+
type: schema.type || null,
|
|
496
|
+
fields: Object.keys(props),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function candidateListingForPlan(listing, requirement) {
|
|
501
|
+
const schemaCheck = validateRequirementAgainstSchema(requirement || {}, listing?.input_schema);
|
|
502
|
+
return {
|
|
503
|
+
...compactListingForPlan(listing),
|
|
504
|
+
description: listing?.description || null,
|
|
505
|
+
tags: Array.isArray(listing?.tags) ? listing.tags : [],
|
|
506
|
+
score: listing?.score ?? null,
|
|
507
|
+
reasons: Array.isArray(listing?.reasons) ? listing.reasons : [],
|
|
508
|
+
input_schema: listing?.input_schema || null,
|
|
509
|
+
output_schema_summary: summarizeOutputSchema(listing?.output_schema),
|
|
510
|
+
schema_compatibility: {
|
|
511
|
+
valid: schemaCheck.valid,
|
|
512
|
+
missing_required_fields: schemaCheck.missing,
|
|
513
|
+
},
|
|
514
|
+
decision: {
|
|
515
|
+
allowed: listing?.policy?.allowed !== false,
|
|
516
|
+
blocked_reasons: listing?.policy?.blocked_reasons || [],
|
|
517
|
+
// Source of truth for verbosity is the server; client never truncates.
|
|
518
|
+
why_matched: listing?.match_explanation || "",
|
|
519
|
+
how_to_use: listing?.invocation_guidance || [],
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function diagnosticStatus(checks) {
|
|
525
|
+
return checks.some((check) => check.status === "fail") ? "fail" : "pass";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function credentialsFileMode(credentialsPath) {
|
|
529
|
+
try {
|
|
530
|
+
return fs.statSync(credentialsPath).mode & 0o777;
|
|
531
|
+
} catch (_err) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function defaultAgentName(env) {
|
|
537
|
+
const base = env.HERMES_AGENT_NAME || env.USER || "HermesAgent";
|
|
538
|
+
return `${base}`.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 48) || "HermesAgent";
|
|
539
|
+
}
|
|
540
|
+
function parseDeliveryNote(deliveryNote) {
|
|
541
|
+
if (!deliveryNote) return { format: "empty", value: null };
|
|
542
|
+
try {
|
|
543
|
+
return { format: "json", value: JSON.parse(deliveryNote) };
|
|
544
|
+
} catch (_err) {
|
|
545
|
+
return { format: "text", value: deliveryNote };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function summarizeOrderMessages(messages, limit = 3) {
|
|
550
|
+
const recent = (Array.isArray(messages) ? messages : [])
|
|
551
|
+
.slice(-limit)
|
|
552
|
+
.map((message) => ({
|
|
553
|
+
id: message?.id || null,
|
|
554
|
+
sender_id: message?.sender_id || null,
|
|
555
|
+
sender_name: message?.sender?.name || null,
|
|
556
|
+
content: message?.content || "",
|
|
557
|
+
created_at: message?.created_at || null,
|
|
558
|
+
}));
|
|
559
|
+
return {
|
|
560
|
+
message_count: Array.isArray(messages) ? messages.length : 0,
|
|
561
|
+
recent_messages: recent,
|
|
562
|
+
latest_message: recent.length > 0 ? recent[recent.length - 1] : null,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function fetchOrderCancellationContext(deps, orderId) {
|
|
567
|
+
try {
|
|
568
|
+
const detail = await requestJson(deps, "GET", `/orders/${orderId}/messages?limit=20`);
|
|
569
|
+
const messages = Array.isArray(detail?.messages) ? detail.messages : [];
|
|
570
|
+
return summarizeOrderMessages(messages);
|
|
571
|
+
} catch (_err) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function fetchOrderAttachments(deps, orderId) {
|
|
577
|
+
try {
|
|
578
|
+
const response = await requestJson(deps, "GET", `/orders/${orderId}/attachments`);
|
|
579
|
+
const files = Array.isArray(response?.files) ? response.files : [];
|
|
580
|
+
const deliveryFiles = files.filter((file) => file?.file_type === "seller_delivery");
|
|
581
|
+
return {
|
|
582
|
+
files,
|
|
583
|
+
delivery_files: deliveryFiles,
|
|
584
|
+
file_count: Number.isFinite(response?.file_count) ? response.file_count : files.length,
|
|
585
|
+
delivery_file_count: deliveryFiles.length,
|
|
586
|
+
total_size: Number.isFinite(response?.total_size)
|
|
587
|
+
? response.total_size
|
|
588
|
+
: files.reduce((sum, file) => sum + (Number(file?.size) || 0), 0),
|
|
589
|
+
};
|
|
590
|
+
} catch (_err) {
|
|
591
|
+
return {
|
|
592
|
+
files: [],
|
|
593
|
+
delivery_files: [],
|
|
594
|
+
file_count: 0,
|
|
595
|
+
delivery_file_count: 0,
|
|
596
|
+
total_size: 0,
|
|
597
|
+
unavailable: true,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function deriveBountyFromGoal(goal, options) {
|
|
602
|
+
const trimmed = goal.trim();
|
|
603
|
+
const title = options["bounty-title"] || (trimmed.length >= 5 ? trimmed.slice(0, 120) : `ClawLabor bounty: ${trimmed}`);
|
|
604
|
+
const baseDescription = options["bounty-description"] || trimmed;
|
|
605
|
+
const description =
|
|
606
|
+
baseDescription.length >= 20
|
|
607
|
+
? baseDescription
|
|
608
|
+
: `${baseDescription}\n\nPosted automatically by clawlabor solve because no listing matched the requested goal.`;
|
|
609
|
+
return { title, description };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
module.exports = {
|
|
613
|
+
ApiError,
|
|
614
|
+
attachmentPath,
|
|
615
|
+
apiBase,
|
|
616
|
+
buildSampleRequirement,
|
|
617
|
+
candidateListingForPlan,
|
|
618
|
+
compactListingForPlan,
|
|
619
|
+
describeRequiredFields,
|
|
620
|
+
credentialState,
|
|
621
|
+
credentialsFileMode,
|
|
622
|
+
credentialsFilePath,
|
|
623
|
+
defaultAgentName,
|
|
624
|
+
deriveBountyFromGoal,
|
|
625
|
+
diagnosticStatus,
|
|
626
|
+
ensureUploadPathAllowed,
|
|
627
|
+
fetchOrderAttachments,
|
|
628
|
+
fetchOrderCancellationContext,
|
|
629
|
+
guessMimeType,
|
|
630
|
+
hasUriSchemaField,
|
|
631
|
+
isStrictUrlField,
|
|
632
|
+
matchBody,
|
|
633
|
+
loadPolicy,
|
|
634
|
+
isUrlField,
|
|
635
|
+
makeIdempotencyKey,
|
|
636
|
+
makePublishIdempotencyKey,
|
|
637
|
+
numberOption,
|
|
638
|
+
parseFileFlags,
|
|
639
|
+
parseDeliveryNote,
|
|
640
|
+
parseInputFlags,
|
|
641
|
+
parseJsonOption,
|
|
642
|
+
parseRequirement,
|
|
643
|
+
pickCompatibleListing,
|
|
644
|
+
positiveNumberOption,
|
|
645
|
+
readAttachmentOptions,
|
|
646
|
+
request,
|
|
647
|
+
requestJson,
|
|
648
|
+
requestJsonNoAuth,
|
|
649
|
+
requestMultipart,
|
|
650
|
+
resolveApiKey,
|
|
651
|
+
requiredOption,
|
|
652
|
+
shellQuote,
|
|
653
|
+
stageAndUploadFile,
|
|
654
|
+
stringOptionFromFile,
|
|
655
|
+
summarizeOrderMessages,
|
|
656
|
+
TERMINAL_ORDER_STATES,
|
|
657
|
+
uploadAttachment,
|
|
658
|
+
validateRequirementAgainstSchema,
|
|
659
|
+
writeCredentialsFile,
|
|
660
|
+
};
|