@carrierllc/mcp 0.9.2 → 0.10.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 +63 -5
- package/dist/{chunk-4XHSOF62.js → chunk-DUAENMJE.js} +325 -4
- package/dist/chunk-DUAENMJE.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1575 -1401
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/plugin/carrier/README.md +1 -1
- package/dist/chunk-4XHSOF62.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CARRIER_VERSION,
|
|
4
|
+
OCS_MAX_USAGE_WINDOW_DAYS,
|
|
4
5
|
ROUTER_RULES,
|
|
5
6
|
TARGET_IDS,
|
|
6
7
|
acquireEndpointSlot,
|
|
8
|
+
applyParamRenames,
|
|
7
9
|
applySubscriberFilters,
|
|
8
10
|
buildListSubscriberParams,
|
|
9
11
|
buildRouterCatalog,
|
|
10
12
|
buildSite,
|
|
11
13
|
clerkCliDeps,
|
|
12
14
|
configureClerkInstance,
|
|
15
|
+
daysUntil,
|
|
13
16
|
deploySite,
|
|
14
17
|
esimStatusPerAccountParams,
|
|
18
|
+
extractDailyUsage,
|
|
15
19
|
extractEsimStatusCounts,
|
|
20
|
+
filterActiveSubscribers,
|
|
16
21
|
fleetScreen,
|
|
22
|
+
formatBytes,
|
|
17
23
|
formatProbes,
|
|
18
24
|
generateStorefrontLogo,
|
|
19
25
|
getLimitForEndpoint,
|
|
20
26
|
getRateLimitWindowCounts,
|
|
21
27
|
imsiFromSubscriberRecord,
|
|
22
28
|
isTargetId,
|
|
29
|
+
lastNDaysPeriod,
|
|
23
30
|
loadStorefrontBrand,
|
|
24
31
|
locationParams,
|
|
25
32
|
mergeEnvLocal,
|
|
@@ -33,11 +40,14 @@ import {
|
|
|
33
40
|
recurringPackageParams,
|
|
34
41
|
renderHtml,
|
|
35
42
|
repairPlanFor,
|
|
43
|
+
runWithBudget,
|
|
36
44
|
storefrontClerkUrls,
|
|
37
45
|
subscriberIdParams,
|
|
46
|
+
subscriberRows,
|
|
38
47
|
usageOverPeriodParams,
|
|
39
|
-
verifyStorefront
|
|
40
|
-
|
|
48
|
+
verifyStorefront,
|
|
49
|
+
withOcsListSummary
|
|
50
|
+
} from "./chunk-DUAENMJE.js";
|
|
41
51
|
import "./chunk-SHKKVIIA.js";
|
|
42
52
|
|
|
43
53
|
// src/index.ts
|
|
@@ -69,32 +79,35 @@ var OcsClient = class {
|
|
|
69
79
|
async call(method, params = {}) {
|
|
70
80
|
await acquireEndpointSlot(this.token, method, "interactive");
|
|
71
81
|
const url = `${this.baseUrl}/v1?token=${this.token}`;
|
|
72
|
-
const body2 = JSON.stringify({ [method]: params });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (json.status?.code !== 0) {
|
|
83
|
-
throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
|
|
84
|
-
}
|
|
85
|
-
if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
|
|
86
|
-
return json["listTariffRule"];
|
|
87
|
-
}
|
|
88
|
-
if (method === "getSubscriberLocationByCellId") {
|
|
89
|
-
const byMethod = json[method];
|
|
90
|
-
if (byMethod !== void 0) {
|
|
91
|
-
return byMethod;
|
|
82
|
+
const body2 = JSON.stringify({ [method]: applyParamRenames(method, params) });
|
|
83
|
+
return runWithBudget(method, async (signal) => {
|
|
84
|
+
const res = await fetch(url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: body2,
|
|
88
|
+
signal
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new OcsApiError(res.status, `HTTP ${res.status} ${res.statusText}`, method);
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const json = await res.json();
|
|
94
|
+
if (json.status?.code !== 0) {
|
|
95
|
+
throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
|
|
98
|
+
return json["listTariffRule"];
|
|
99
|
+
}
|
|
100
|
+
if (method === "getSubscriberLocationByCellId") {
|
|
101
|
+
const byMethod = json[method];
|
|
102
|
+
if (byMethod !== void 0) {
|
|
103
|
+
return byMethod;
|
|
104
|
+
}
|
|
105
|
+
if (json["subscriberLocation"] !== void 0) {
|
|
106
|
+
return json["subscriberLocation"];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return json[method] ?? json;
|
|
110
|
+
});
|
|
98
111
|
}
|
|
99
112
|
};
|
|
100
113
|
|
|
@@ -189,6 +202,9 @@ function estimateCostUsd(u, rates = HAIKU_RATES) {
|
|
|
189
202
|
var DEFAULT_BEDROCK_REGION = "us-east-1";
|
|
190
203
|
var DEFAULT_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
|
|
191
204
|
var BEDROCK_TIMEOUT_MS = 3e4;
|
|
205
|
+
var MAX_ATTEMPTS = 4;
|
|
206
|
+
var INITIAL_RETRY_MS = 100;
|
|
207
|
+
var MIN_ATTEMPT_BUDGET_MS = 250;
|
|
192
208
|
var BedrockRateLimitError = class extends Error {
|
|
193
209
|
isRateLimit = true;
|
|
194
210
|
/**
|
|
@@ -218,7 +234,11 @@ function client(creds) {
|
|
|
218
234
|
secretAccessKey: creds.secretAccessKey,
|
|
219
235
|
...creds.sessionToken ? { sessionToken: creds.sessionToken } : {},
|
|
220
236
|
region,
|
|
221
|
-
service: "bedrock"
|
|
237
|
+
service: "bedrock",
|
|
238
|
+
// See MAX_ATTEMPTS. aws4fetch's retry loop cannot be cancelled, so this
|
|
239
|
+
// layer owns the policy instead. Changing this back to the default 10
|
|
240
|
+
// silently restores the ~50s stall that timeoutMs is supposed to prevent.
|
|
241
|
+
retries: 0
|
|
222
242
|
}),
|
|
223
243
|
region,
|
|
224
244
|
modelId: creds.modelId ?? DEFAULT_MODEL_ID
|
|
@@ -260,6 +280,47 @@ async function fail(resp) {
|
|
|
260
280
|
}
|
|
261
281
|
throw new BedrockError(text || `Bedrock returned HTTP ${resp.status}`, resp.status);
|
|
262
282
|
}
|
|
283
|
+
function timedOut() {
|
|
284
|
+
const err7 = new Error("Bedrock call timed out before a usable response");
|
|
285
|
+
err7.name = "TimeoutError";
|
|
286
|
+
return err7;
|
|
287
|
+
}
|
|
288
|
+
function sleep(ms, signal) {
|
|
289
|
+
if (signal.aborted) return Promise.resolve();
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const done = () => {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
signal.removeEventListener("abort", done);
|
|
294
|
+
resolve();
|
|
295
|
+
};
|
|
296
|
+
const timer = setTimeout(done, ms);
|
|
297
|
+
signal.addEventListener("abort", done, { once: true });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function retryable(status) {
|
|
301
|
+
return status === 429 || status >= 500;
|
|
302
|
+
}
|
|
303
|
+
async function sendWithRetry(aws, url, init, signal, deadlineAt) {
|
|
304
|
+
let throttled = null;
|
|
305
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
306
|
+
if (signal.aborted) break;
|
|
307
|
+
let resp;
|
|
308
|
+
try {
|
|
309
|
+
resp = await aws.fetch(url, init);
|
|
310
|
+
} catch (err7) {
|
|
311
|
+
if (signal.aborted && throttled) return throttled;
|
|
312
|
+
throw err7;
|
|
313
|
+
}
|
|
314
|
+
if (!retryable(resp.status)) return resp;
|
|
315
|
+
throttled = resp;
|
|
316
|
+
if (attempt === MAX_ATTEMPTS - 1) break;
|
|
317
|
+
const backoff = Math.random() * INITIAL_RETRY_MS * 2 ** attempt;
|
|
318
|
+
if (Date.now() + backoff + MIN_ATTEMPT_BUDGET_MS > deadlineAt) break;
|
|
319
|
+
await sleep(backoff, signal);
|
|
320
|
+
}
|
|
321
|
+
if (throttled) return throttled;
|
|
322
|
+
throw timedOut();
|
|
323
|
+
}
|
|
263
324
|
async function invokeTool(creds, req, signal) {
|
|
264
325
|
const json = await invokeModelRaw(
|
|
265
326
|
creds,
|
|
@@ -273,19 +334,24 @@ async function invokeTool(creds, req, signal) {
|
|
|
273
334
|
}
|
|
274
335
|
async function invokeModelRaw(creds, payload, opts = {}, signal) {
|
|
275
336
|
const { aws, region, modelId } = client(creds);
|
|
337
|
+
const timeoutMs = opts.timeoutMs ?? BEDROCK_TIMEOUT_MS;
|
|
338
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
276
339
|
const controller = new AbortController();
|
|
277
|
-
const timer = setTimeout(() => controller.abort(),
|
|
340
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
278
341
|
const onAbort = () => controller.abort();
|
|
279
342
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
280
343
|
try {
|
|
281
|
-
const resp = await
|
|
344
|
+
const resp = await sendWithRetry(
|
|
345
|
+
aws,
|
|
282
346
|
`https://bedrock-runtime.${region}.amazonaws.com/model/${encodeURIComponent(modelId)}/invoke`,
|
|
283
347
|
{
|
|
284
348
|
method: "POST",
|
|
285
349
|
headers: headersFor(opts.attribution, "application/json"),
|
|
286
350
|
body: JSON.stringify(payload),
|
|
287
351
|
signal: controller.signal
|
|
288
|
-
}
|
|
352
|
+
},
|
|
353
|
+
controller.signal,
|
|
354
|
+
deadlineAt
|
|
289
355
|
);
|
|
290
356
|
if (!resp.ok) await fail(resp);
|
|
291
357
|
return await resp.json();
|
|
@@ -294,6 +360,469 @@ async function invokeModelRaw(creds, payload, opts = {}, signal) {
|
|
|
294
360
|
signal?.removeEventListener("abort", onAbort);
|
|
295
361
|
}
|
|
296
362
|
}
|
|
363
|
+
var PENDING_ASK_PREFIX = "manus_pending_ask:";
|
|
364
|
+
var PENDING_ASK_TTL_SECONDS = 24 * 3600;
|
|
365
|
+
var OCS_DASHBOARD_DEFAULT = "https://ocs.esimvault.cloud";
|
|
366
|
+
function buildLoginPreamble(dashboardUrl) {
|
|
367
|
+
return `STEP 1 \u2014 LOGIN:
|
|
368
|
+
Navigate to ${dashboardUrl}/login (or the main page if no /login path).
|
|
369
|
+
Enter the OCS portal username and password provided below.
|
|
370
|
+
Wait for the dashboard to fully load after login.
|
|
371
|
+
If already logged in (session cookie persists), skip to STEP 2.
|
|
372
|
+
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
function assembleUiAgentPrompt(operationPrompt, credentials, dashboardUrl) {
|
|
376
|
+
return `You are a Carrier MCP UI automation agent. Your task is to perform an OCS dashboard operation that is not available via the OCS REST API.
|
|
377
|
+
|
|
378
|
+
OCS PORTAL CREDENTIALS (use these to log in \u2014 NEVER include them in your output):
|
|
379
|
+
Username: ${credentials.username}
|
|
380
|
+
Password: ${credentials.password}
|
|
381
|
+
|
|
382
|
+
` + buildLoginPreamble(dashboardUrl) + `STEP 2 \u2014 OPERATION:
|
|
383
|
+
` + operationPrompt + `
|
|
384
|
+
|
|
385
|
+
STEP 3 \u2014 VERIFICATION:
|
|
386
|
+
After completing the operation, verify the result by checking the dashboard shows the expected state.
|
|
387
|
+
Take a screenshot of the final state for audit purposes.
|
|
388
|
+
Report success or failure with a clear summary.`;
|
|
389
|
+
}
|
|
390
|
+
function redactCredentials(prompt, credentials) {
|
|
391
|
+
return prompt.replaceAll(credentials.password, "***REDACTED***").replaceAll(credentials.username, "***REDACTED***");
|
|
392
|
+
}
|
|
393
|
+
function buildCreateSteeringListPrompt(params, dashboardUrl) {
|
|
394
|
+
return `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
395
|
+
Create a new steering list with the following details:
|
|
396
|
+
Name: ${params.name}
|
|
397
|
+
` + (params.description ? ` Description: ${params.description}
|
|
398
|
+
` : "") + `Click the "Create" or "Add" button to create the steering list.
|
|
399
|
+
After creation, note the new steering list ID from the dashboard.
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
function buildBuildSteeringListPrompt(params, dashboardUrl) {
|
|
403
|
+
const add = params.add_operators ?? [];
|
|
404
|
+
const remove = params.remove_operators ?? [];
|
|
405
|
+
return `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
406
|
+
Open steering list ID ${params.steering_list_id} for editing.
|
|
407
|
+
Operator type: ${params.operator_type ?? "priority"}
|
|
408
|
+
` + (add.length > 0 ? `Add the following operators: ${add.join(", ")}
|
|
409
|
+
` : "") + (remove.length > 0 ? `Remove the following operators: ${remove.join(", ")}
|
|
410
|
+
` : "") + `Save the changes and verify the updated operator list.
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
function buildSetAccountSteeringListPrompt(params, dashboardUrl) {
|
|
414
|
+
return `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
|
|
415
|
+
Open account ID ${params.account_id}.
|
|
416
|
+
` + (params.steering_list_id === 0 ? `Remove/unset the steering list assignment from this account.
|
|
417
|
+
` : `Assign steering list ID ${params.steering_list_id} to this account.
|
|
418
|
+
`) + `Save the changes and verify the steering list assignment is updated.
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
function buildCreateAccountPrompt(params, dashboardUrl) {
|
|
422
|
+
return `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
|
|
423
|
+
Create a new account with the following details:
|
|
424
|
+
Name: ${params.name}
|
|
425
|
+
` + (params.description ? ` Description: ${params.description}
|
|
426
|
+
` : "") + (params.initial_balance ? ` Initial balance: ${params.initial_balance}
|
|
427
|
+
` : "") + `Click the "Create" or "Add" button.
|
|
428
|
+
After creation, note the new account ID from the dashboard.
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
function buildCreateDestinationListPrompt(params, dashboardUrl) {
|
|
432
|
+
const prefixes = params.prefixes ?? [];
|
|
433
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
434
|
+
Create a new destination list with the following details:
|
|
435
|
+
Name: ${params.name}
|
|
436
|
+
` + (params.description ? ` Description: ${params.description}
|
|
437
|
+
` : "") + (prefixes.length > 0 ? ` Prefixes to add: ${prefixes.join(", ")}
|
|
438
|
+
` : "") + `Save the new destination list and note the ID.
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
function buildEditDestinationListPrompt(params, dashboardUrl) {
|
|
442
|
+
const add = params.add_prefixes ?? [];
|
|
443
|
+
const remove = params.remove_prefixes ?? [];
|
|
444
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
445
|
+
Open destination list ID ${params.destination_list_id} for editing.
|
|
446
|
+
` + (params.new_name ? `Rename to: ${params.new_name}
|
|
447
|
+
` : "") + (add.length > 0 ? `Add prefixes: ${add.join(", ")}
|
|
448
|
+
` : "") + (remove.length > 0 ? `Remove prefixes: ${remove.join(", ")}
|
|
449
|
+
` : "") + `Save the changes and verify the updated prefix list.
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
function buildDeleteDestinationListPrompt(params, dashboardUrl) {
|
|
453
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
454
|
+
Find destination list ID ${params.destination_list_id}.
|
|
455
|
+
Delete this destination list. Confirm the deletion when prompted.
|
|
456
|
+
Verify the list no longer appears in the dashboard.
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
function buildDeletePackageTemplatePrompt(params, dashboardUrl) {
|
|
460
|
+
return `Navigate to the Package Templates section of the OCS dashboard at ${dashboardUrl}.
|
|
461
|
+
Find package template ID ${params.template_id}.
|
|
462
|
+
Delete this package template. Confirm the deletion when prompted.
|
|
463
|
+
Verify the template no longer appears in the template list.
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
function buildEditLocationZonePrompt(params, dashboardUrl) {
|
|
467
|
+
const add = params.add_countries ?? [];
|
|
468
|
+
const remove = params.remove_countries ?? [];
|
|
469
|
+
return `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
|
|
470
|
+
Open location zone ID ${params.zone_id} for editing.
|
|
471
|
+
` + (params.new_name ? `Rename to: ${params.new_name}
|
|
472
|
+
` : "") + (add.length > 0 ? `Add countries: ${add.join(", ")}
|
|
473
|
+
` : "") + (remove.length > 0 ? `Remove countries: ${remove.join(", ")}
|
|
474
|
+
` : "") + `Save the changes and verify the updated country list.
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
function buildDeleteLocationZonePrompt(params, dashboardUrl) {
|
|
478
|
+
return `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
|
|
479
|
+
Find location zone ID ${params.zone_id}.
|
|
480
|
+
Delete this location zone. Confirm the deletion when prompted.
|
|
481
|
+
If the dashboard shows an error (e.g. zone in use by active templates), report the error.
|
|
482
|
+
Verify the zone no longer appears in the zone list.
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
var BROWSER_USE_API = "https://api.browser-use.com/api/v4";
|
|
486
|
+
var RUN_TIMEOUT_MS = 10 * 6e4;
|
|
487
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
488
|
+
function browserUseConfigured(env) {
|
|
489
|
+
return Boolean(env.apiKey);
|
|
490
|
+
}
|
|
491
|
+
function apiKey(env) {
|
|
492
|
+
const key = env.apiKey;
|
|
493
|
+
if (!key) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
"BROWSER_USE_API_KEY is not set \u2014 the browser-use provider cannot run."
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return key;
|
|
499
|
+
}
|
|
500
|
+
async function v4(env, path, init) {
|
|
501
|
+
const res = await fetch(`${BROWSER_USE_API}${path}`, {
|
|
502
|
+
method: init?.method ?? "GET",
|
|
503
|
+
headers: {
|
|
504
|
+
"X-Browser-Use-API-Key": apiKey(env),
|
|
505
|
+
"Content-Type": "application/json"
|
|
506
|
+
},
|
|
507
|
+
...init?.body ? { body: JSON.stringify(init.body) } : {}
|
|
508
|
+
});
|
|
509
|
+
let data = null;
|
|
510
|
+
try {
|
|
511
|
+
data = await res.json();
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
return { ok: res.ok, status: res.status, data };
|
|
515
|
+
}
|
|
516
|
+
function mapStatus(raw) {
|
|
517
|
+
switch ((raw ?? "").toLowerCase()) {
|
|
518
|
+
case "finished":
|
|
519
|
+
case "completed":
|
|
520
|
+
case "success":
|
|
521
|
+
case "stopped":
|
|
522
|
+
return "stopped";
|
|
523
|
+
case "failed":
|
|
524
|
+
case "error":
|
|
525
|
+
return "error";
|
|
526
|
+
case "paused":
|
|
527
|
+
case "waiting":
|
|
528
|
+
case "needs_input":
|
|
529
|
+
return "waiting";
|
|
530
|
+
default:
|
|
531
|
+
return "running";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function defaultRedact(text) {
|
|
535
|
+
return text.replace(/(\bpassword\b\s*[:=]\s*)\S+/gi, "$1***REDACTED***").replace(/(\busername\b\s*[:=]\s*)\S+/gi, "$1***REDACTED***");
|
|
536
|
+
}
|
|
537
|
+
function toTaskResult(run, redact) {
|
|
538
|
+
const status = mapStatus(run.status);
|
|
539
|
+
const scrub = (text) => defaultRedact(redact ? redact(text) : text);
|
|
540
|
+
const payload = run.result ?? run.output;
|
|
541
|
+
if (payload && typeof payload === "object") {
|
|
542
|
+
const p = payload;
|
|
543
|
+
return {
|
|
544
|
+
success: status === "stopped" && !run.error,
|
|
545
|
+
summary: scrub(
|
|
546
|
+
typeof p.summary === "string" ? p.summary : JSON.stringify(payload)
|
|
547
|
+
).slice(0, 2e3),
|
|
548
|
+
...typeof p.entity_id === "string" ? { entity_id: p.entity_id } : {},
|
|
549
|
+
...run.error ? { error_message: scrub(run.error) } : {},
|
|
550
|
+
// The hosted agent does not report a screenshot count. Zero is honest;
|
|
551
|
+
// inventing one would put a fabricated number in an audit row.
|
|
552
|
+
screenshots_taken: 0
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
success: status === "stopped" && !run.error,
|
|
557
|
+
summary: typeof payload === "string" ? scrub(payload).slice(0, 2e3) : "",
|
|
558
|
+
...run.error ? { error_message: scrub(run.error) } : {},
|
|
559
|
+
screenshots_taken: 0
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function viewerUrl(run) {
|
|
563
|
+
return run.sessionId ? `https://cloud.browser-use.com/sessions/${run.sessionId}` : void 0;
|
|
564
|
+
}
|
|
565
|
+
function runSpend(run) {
|
|
566
|
+
if (!run || typeof run.totalCostUsd !== "number") return null;
|
|
567
|
+
return {
|
|
568
|
+
costUsd: run.totalCostUsd,
|
|
569
|
+
inputTokens: run.totalInputTokens ?? 0,
|
|
570
|
+
outputTokens: run.totalOutputTokens ?? 0
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function createRun(env, task, opts) {
|
|
574
|
+
const body2 = { task };
|
|
575
|
+
if (opts?.model) body2.model = opts.model;
|
|
576
|
+
if (opts?.outputSchema) body2.schema = opts.outputSchema;
|
|
577
|
+
const res = await v4(env, "/runs", { method: "POST", body: body2 });
|
|
578
|
+
if (!res.ok) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Browser Use run create failed: HTTP ${res.status} ${JSON.stringify(res.data).slice(0, 300)}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const run = res.data;
|
|
584
|
+
if (!run?.id) throw new Error("Browser Use run create returned no id");
|
|
585
|
+
const url = viewerUrl(run);
|
|
586
|
+
return { runId: run.id, ...url ? { sessionViewerUrl: url } : {} };
|
|
587
|
+
}
|
|
588
|
+
async function getRun(env, runId) {
|
|
589
|
+
const res = await v4(env, `/runs/${encodeURIComponent(runId)}`);
|
|
590
|
+
if (!res.ok) return null;
|
|
591
|
+
return res.data;
|
|
592
|
+
}
|
|
593
|
+
async function waitForRun(env, runId, timeoutMs = RUN_TIMEOUT_MS) {
|
|
594
|
+
const deadline = Date.now() + timeoutMs;
|
|
595
|
+
let last = null;
|
|
596
|
+
while (Date.now() < deadline) {
|
|
597
|
+
last = await getRun(env, runId);
|
|
598
|
+
if (last && mapStatus(last.status) !== "running") return last;
|
|
599
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
600
|
+
}
|
|
601
|
+
return last;
|
|
602
|
+
}
|
|
603
|
+
function toSteelTask(taskId, run, opts) {
|
|
604
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
605
|
+
const status = mapStatus(run?.status);
|
|
606
|
+
const url = run ? viewerUrl(run) : void 0;
|
|
607
|
+
return {
|
|
608
|
+
task_id: taskId,
|
|
609
|
+
status,
|
|
610
|
+
...opts?.ownerSub ? { owner_sub: opts.ownerSub } : {},
|
|
611
|
+
// The console's task detail view renders steel_session_id. It is the
|
|
612
|
+
// provider's session id, whoever the provider is.
|
|
613
|
+
...run?.sessionId ? { steel_session_id: run.sessionId } : {},
|
|
614
|
+
...run?.id ? { run_id: run.id } : {},
|
|
615
|
+
...url ? { session_viewer_url: url } : {},
|
|
616
|
+
...opts?.title ? { title: opts.title } : {},
|
|
617
|
+
created_at: opts?.createdAt ?? now,
|
|
618
|
+
updated_at: now,
|
|
619
|
+
...run && status !== "running" ? { result: toTaskResult(run, opts?.redact) } : {},
|
|
620
|
+
// Scrubbed on the same grounds as the result: the question is agent-authored
|
|
621
|
+
// text, and an agent that pauses to ask for help is exactly the one likely
|
|
622
|
+
// to quote its own instructions — password line included — while explaining
|
|
623
|
+
// what it is stuck on. This string is stored in KV and handed to operators
|
|
624
|
+
// by ui_agent_list_pending.
|
|
625
|
+
...status === "waiting" ? { pending_question: pendingQuestion(run, opts?.redact) } : {}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function pendingQuestion(run, redact) {
|
|
629
|
+
const payload = run?.result;
|
|
630
|
+
const q = payload && typeof payload === "object" ? payload.question : void 0;
|
|
631
|
+
if (typeof q !== "string" || q.length === 0) {
|
|
632
|
+
return "The browser agent is paused and needs input to continue.";
|
|
633
|
+
}
|
|
634
|
+
return defaultRedact(redact ? redact(q) : q);
|
|
635
|
+
}
|
|
636
|
+
function taskKey(taskId) {
|
|
637
|
+
return `steel_task:${taskId}`;
|
|
638
|
+
}
|
|
639
|
+
var TASK_TTL_SECONDS = 7 * 24 * 3600;
|
|
640
|
+
async function beginUiAgentRun(prompt, title, env, opts) {
|
|
641
|
+
if (!browserUseConfigured(env)) {
|
|
642
|
+
throw new Error("BROWSER_USE_API_KEY is not configured");
|
|
643
|
+
}
|
|
644
|
+
const taskId = opts?.taskId ?? `bu_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
645
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
646
|
+
const base = {
|
|
647
|
+
task_id: taskId,
|
|
648
|
+
status: "running",
|
|
649
|
+
title,
|
|
650
|
+
created_at: createdAt,
|
|
651
|
+
updated_at: createdAt,
|
|
652
|
+
...opts?.ownerSub ? { owner_sub: opts.ownerSub } : {}
|
|
653
|
+
};
|
|
654
|
+
await putTask(env, base);
|
|
655
|
+
let created;
|
|
656
|
+
try {
|
|
657
|
+
created = await createRun(env, prompt, {
|
|
658
|
+
...opts?.model ? { model: opts.model } : {},
|
|
659
|
+
...opts?.outputSchema ? { outputSchema: opts.outputSchema } : {}
|
|
660
|
+
});
|
|
661
|
+
} catch (err7) {
|
|
662
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
663
|
+
await putTask(env, {
|
|
664
|
+
...base,
|
|
665
|
+
status: "error",
|
|
666
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
667
|
+
result: {
|
|
668
|
+
success: false,
|
|
669
|
+
summary: `Failed to start a Browser Use run: ${message}`,
|
|
670
|
+
error_message: message,
|
|
671
|
+
screenshots_taken: 0
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
throw err7;
|
|
675
|
+
}
|
|
676
|
+
const dispatched = {
|
|
677
|
+
...base,
|
|
678
|
+
run_id: created.runId,
|
|
679
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
680
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
681
|
+
};
|
|
682
|
+
await putTask(env, dispatched);
|
|
683
|
+
const complete = async () => {
|
|
684
|
+
let run;
|
|
685
|
+
try {
|
|
686
|
+
run = await waitForRun(env, created.runId);
|
|
687
|
+
} catch (err7) {
|
|
688
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
689
|
+
const failed = {
|
|
690
|
+
success: false,
|
|
691
|
+
summary: `Browser Use run failed: ${message}`,
|
|
692
|
+
error_message: message,
|
|
693
|
+
screenshots_taken: 0
|
|
694
|
+
};
|
|
695
|
+
await putTask(env, {
|
|
696
|
+
...dispatched,
|
|
697
|
+
status: "error",
|
|
698
|
+
result: failed,
|
|
699
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
700
|
+
});
|
|
701
|
+
writeAgentAudit(env, {
|
|
702
|
+
event_type: "task_stopped",
|
|
703
|
+
stop_reason: "error",
|
|
704
|
+
task_id: taskId
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
task_id: taskId,
|
|
708
|
+
status: "error",
|
|
709
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
710
|
+
result: failed
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const final = toSteelTask(taskId, run, {
|
|
714
|
+
title,
|
|
715
|
+
...opts?.ownerSub ? { ownerSub: opts.ownerSub } : {},
|
|
716
|
+
...opts?.redact ? { redact: opts.redact } : {},
|
|
717
|
+
createdAt
|
|
718
|
+
});
|
|
719
|
+
if (!final.session_viewer_url && created.sessionViewerUrl) {
|
|
720
|
+
final.session_viewer_url = created.sessionViewerUrl;
|
|
721
|
+
}
|
|
722
|
+
final.run_id = created.runId;
|
|
723
|
+
await putTask(env, final);
|
|
724
|
+
if (final.status === "waiting") {
|
|
725
|
+
await writePendingAsk(env, final);
|
|
726
|
+
}
|
|
727
|
+
const spend = runSpend(run);
|
|
728
|
+
writeAgentAudit(env, {
|
|
729
|
+
event_type: "task_stopped",
|
|
730
|
+
stop_reason: final.status === "waiting" ? "ask" : final.status === "stopped" ? "finish" : final.status === "running" ? "timeout" : "error",
|
|
731
|
+
task_id: taskId,
|
|
732
|
+
...spend ? { spend } : {}
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
task_id: taskId,
|
|
736
|
+
status: final.status,
|
|
737
|
+
...final.session_viewer_url ? { session_viewer_url: final.session_viewer_url } : {},
|
|
738
|
+
...final.result ? { result: final.result } : {},
|
|
739
|
+
...spend ? { spend } : {}
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
task_id: taskId,
|
|
744
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
745
|
+
complete
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async function runUiAgent(prompt, title, env, opts) {
|
|
749
|
+
const handle = await beginUiAgentRun(prompt, title, env, opts);
|
|
750
|
+
return await handle.complete();
|
|
751
|
+
}
|
|
752
|
+
async function getUiAgentTask(taskId, env) {
|
|
753
|
+
const raw = await env.store.get(taskKey(taskId));
|
|
754
|
+
if (!raw) return null;
|
|
755
|
+
try {
|
|
756
|
+
return JSON.parse(raw);
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function resumeUiAgentTask(taskId, reply, env) {
|
|
762
|
+
const pendingRaw = await env.store.get(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
763
|
+
if (!pendingRaw) {
|
|
764
|
+
return { ok: false, error: "No pending-ask entry found for this task_id." };
|
|
765
|
+
}
|
|
766
|
+
const task = await getUiAgentTask(taskId, env);
|
|
767
|
+
if (!task) {
|
|
768
|
+
return { ok: false, error: "Task record not found." };
|
|
769
|
+
}
|
|
770
|
+
await env.store.delete(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
771
|
+
const { pending_question: _dropped, ...rest } = task;
|
|
772
|
+
await putTask(env, {
|
|
773
|
+
...rest,
|
|
774
|
+
status: "running",
|
|
775
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
776
|
+
});
|
|
777
|
+
writeAgentAudit(env, {
|
|
778
|
+
event_type: "task_stopped",
|
|
779
|
+
stop_reason: "resumed",
|
|
780
|
+
task_id: taskId,
|
|
781
|
+
replyLen: reply.length
|
|
782
|
+
});
|
|
783
|
+
return { ok: true };
|
|
784
|
+
}
|
|
785
|
+
async function putTask(env, task) {
|
|
786
|
+
await env.store.put(taskKey(task.task_id), JSON.stringify(task), {
|
|
787
|
+
expirationTtl: TASK_TTL_SECONDS
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
async function writePendingAsk(env, task) {
|
|
791
|
+
const entry = {
|
|
792
|
+
task_id: task.task_id,
|
|
793
|
+
question: task.pending_question ?? "The browser agent is paused and needs input to continue.",
|
|
794
|
+
task_url: task.session_viewer_url ?? "",
|
|
795
|
+
asked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
796
|
+
};
|
|
797
|
+
await env.store.put(
|
|
798
|
+
`${PENDING_ASK_PREFIX}${task.task_id}`,
|
|
799
|
+
JSON.stringify(entry),
|
|
800
|
+
{ expirationTtl: PENDING_ASK_TTL_SECONDS }
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
function writeAgentAudit(env, entry) {
|
|
804
|
+
try {
|
|
805
|
+
env.audit({
|
|
806
|
+
blobs: [
|
|
807
|
+
"steel_agent",
|
|
808
|
+
entry.event_type,
|
|
809
|
+
entry.stop_reason,
|
|
810
|
+
entry.task_id,
|
|
811
|
+
"0",
|
|
812
|
+
"browser_use"
|
|
813
|
+
],
|
|
814
|
+
doubles: [
|
|
815
|
+
entry.replyLen ?? 0,
|
|
816
|
+
entry.spend?.costUsd ?? 0,
|
|
817
|
+
entry.spend?.inputTokens ?? 0,
|
|
818
|
+
entry.spend?.outputTokens ?? 0
|
|
819
|
+
],
|
|
820
|
+
indexes: [entry.task_id]
|
|
821
|
+
});
|
|
822
|
+
} catch (err7) {
|
|
823
|
+
console.error(`[browser-use-agent] audit write failed: ${err7.message}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
297
826
|
|
|
298
827
|
// src/billing.ts
|
|
299
828
|
var TIER_CALL_LIMITS = {
|
|
@@ -769,11 +1298,12 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
769
1298
|
return result2;
|
|
770
1299
|
};
|
|
771
1300
|
}
|
|
772
|
-
async function ocsCall(env, token2, method, params = {}) {
|
|
1301
|
+
async function ocsCall(env, token2, method, params = {}, listKind) {
|
|
773
1302
|
const client2 = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
|
|
774
1303
|
const result2 = await client2.call(method, params);
|
|
1304
|
+
const payload = listKind ? withOcsListSummary(result2, listKind) : result2;
|
|
775
1305
|
return {
|
|
776
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
1306
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
777
1307
|
};
|
|
778
1308
|
}
|
|
779
1309
|
async function resolveSubscriberByIccid(env, token2, iccid, cache) {
|
|
@@ -803,7 +1333,7 @@ function registerAllTools(server2, ctx) {
|
|
|
803
1333
|
"list_reseller_accounts",
|
|
804
1334
|
{
|
|
805
1335
|
title: "List Reseller Accounts",
|
|
806
|
-
description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
|
|
1336
|
+
description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ summary, reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. `summary.total` therefore counts RESELLERS; the account count is `summary.nested.accounts`. For any question about accounts use that, never `total`. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
|
|
807
1337
|
inputSchema: {
|
|
808
1338
|
resellerId: z.number().optional().describe("Filter to a specific reseller by ID (omit for token owner's reseller)")
|
|
809
1339
|
},
|
|
@@ -817,7 +1347,7 @@ function registerAllTools(server2, ctx) {
|
|
|
817
1347
|
async ({ resellerId }, token2) => {
|
|
818
1348
|
const params = {};
|
|
819
1349
|
if (resellerId !== void 0) params.resellerId = resellerId;
|
|
820
|
-
return ocsCall(ctx.env, token2, "listResellerAccount", params);
|
|
1350
|
+
return ocsCall(ctx.env, token2, "listResellerAccount", params, "resellers");
|
|
821
1351
|
}
|
|
822
1352
|
)
|
|
823
1353
|
);
|
|
@@ -965,7 +1495,7 @@ function registerAllTools(server2, ctx) {
|
|
|
965
1495
|
"list_subscribers",
|
|
966
1496
|
{
|
|
967
1497
|
title: "List Subscribers",
|
|
968
|
-
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns:
|
|
1498
|
+
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns: `{ summary, subscriberList: [...] }` \u2014 subscriber summary records with ICCID, status and account, behind a server-counted `summary.total` (rows returned) and `summary.breakdown.status`. Answer 'how many' from `summary`, not by counting rows \u2014 but when `summary.moreAvailable` is set this is one page, so report `summary.upstreamTotal` for the fleet-wide figure, never `total`. Do NOT use this to fetch full details for a specific subscriber \u2014 use `get_subscriber` for that.",
|
|
969
1499
|
inputSchema: z.object({
|
|
970
1500
|
imsi: z.string().optional().describe("Filter by IMSI"),
|
|
971
1501
|
iccid: z.string().optional().describe("Filter by ICCID"),
|
|
@@ -994,7 +1524,12 @@ function registerAllTools(server2, ctx) {
|
|
|
994
1524
|
const raw = await client2.call("listSubscriber", params);
|
|
995
1525
|
const payload = applySubscriberFilters(raw, args);
|
|
996
1526
|
return {
|
|
997
|
-
content: [
|
|
1527
|
+
content: [
|
|
1528
|
+
{
|
|
1529
|
+
type: "text",
|
|
1530
|
+
text: JSON.stringify(withOcsListSummary(payload, "subscribers"), null, 2)
|
|
1531
|
+
}
|
|
1532
|
+
]
|
|
998
1533
|
};
|
|
999
1534
|
}
|
|
1000
1535
|
)
|
|
@@ -1321,7 +1856,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1321
1856
|
"list_subscriber_packages",
|
|
1322
1857
|
{
|
|
1323
1858
|
title: "List Subscriber Packages",
|
|
1324
|
-
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns:
|
|
1859
|
+
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns: `{ summary, prepaidPackage: [...] }` \u2014 package records with `packageId`, `name`, `status`, `dataLimit`, `dataUsed`, `expirationDate` and a `recurring` flag, behind a server-counted `summary.total` and a `summary.breakdown` over status and recurring. Answer 'how many' from `summary`, not by counting rows. Do NOT use this to browse the product catalog \u2014 use `list_package_templates` for that.",
|
|
1325
1860
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
1326
1861
|
annotations: { readOnlyHint: true }
|
|
1327
1862
|
},
|
|
@@ -1330,7 +1865,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1330
1865
|
"listSubscriberPrepaidPackages",
|
|
1331
1866
|
TOOL_SCOPES["list_subscriber_packages"],
|
|
1332
1867
|
ctx,
|
|
1333
|
-
async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
|
|
1868
|
+
async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }, "packages")
|
|
1334
1869
|
)
|
|
1335
1870
|
);
|
|
1336
1871
|
server2.registerTool(
|
|
@@ -1563,10 +2098,11 @@ function registerAllTools(server2, ctx) {
|
|
|
1563
2098
|
"list_package_templates",
|
|
1564
2099
|
{
|
|
1565
2100
|
title: "List Package Templates",
|
|
1566
|
-
description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: `
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2101
|
+
description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: none \u2014 returns the whole catalog for the token owner's reseller. Returns: `{ summary, template: [...] }`. `summary.total` is the number of templates, counted by the server over the complete catalog, and `summary.breakdown` counts the templates per `recurring` value and per location zone. Answer any 'how many' question from `summary` \u2014 it is authoritative, and counting the rows yourself gets it wrong. Each row carries `prepaidpackagetemplateid`, `prepaidpackagetemplatename`, `databyte` (bytes), `cost`, `perioddays`, `locationzoneid`, `rdbLocationZones.locationzonename` and `recurring`. Do NOT use this to list packages assigned to a specific subscriber \u2014 use `list_subscriber_packages`.",
|
|
2102
|
+
// No `accountId` — see the matching note in apps/mcp-server/src/tools.ts.
|
|
2103
|
+
// OCS rejected every call that carried it, and it is not a rename of any
|
|
2104
|
+
// of the five properties OCS does accept.
|
|
2105
|
+
inputSchema: {},
|
|
1570
2106
|
annotations: { readOnlyHint: true }
|
|
1571
2107
|
},
|
|
1572
2108
|
wrapHandler(
|
|
@@ -1574,11 +2110,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1574
2110
|
"listPrepaidPackageTemplate",
|
|
1575
2111
|
TOOL_SCOPES["list_package_templates"],
|
|
1576
2112
|
ctx,
|
|
1577
|
-
async (
|
|
1578
|
-
const params = {};
|
|
1579
|
-
if (accountId !== void 0) params.accountId = accountId;
|
|
1580
|
-
return ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", params);
|
|
1581
|
-
}
|
|
2113
|
+
async (_args, token2) => ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", {}, "templates")
|
|
1582
2114
|
)
|
|
1583
2115
|
);
|
|
1584
2116
|
server2.registerTool(
|
|
@@ -1831,7 +2363,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1831
2363
|
"get_tariff",
|
|
1832
2364
|
{
|
|
1833
2365
|
title: "Get Customer Tariff",
|
|
1834
|
-
description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
|
|
2366
|
+
description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. This is the slowest tool on the platform: the OCS table is ~3.4 MB and takes 3 s in a fast window and up to ~2 minutes in a slow one, so the call carries a 180 s budget. Expect to wait, and do not call it in a loop. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
|
|
1835
2367
|
inputSchema: {
|
|
1836
2368
|
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
|
|
1837
2369
|
country: z.string().optional().describe(
|
|
@@ -1957,8 +2489,9 @@ var ocs_methods_default = {
|
|
|
1957
2489
|
scope: "read",
|
|
1958
2490
|
description: "Retrieve reseller details",
|
|
1959
2491
|
params: {
|
|
1960
|
-
resellerId: { type: "number", required: false }
|
|
2492
|
+
resellerId: { type: "number", required: false, ocs_field: "id" }
|
|
1961
2493
|
},
|
|
2494
|
+
params_note: "`resellerId` is the MCP-facing name. OCS getResellerInfo accepts exactly one property, `id`; @carrier/ocs-client translates it at the serialisation chokepoint (OCS_PARAM_RENAMES).",
|
|
1962
2495
|
response: {},
|
|
1963
2496
|
annotations: "readOnlyHint",
|
|
1964
2497
|
verified_against_server: true,
|
|
@@ -2363,9 +2896,8 @@ var ocs_methods_default = {
|
|
|
2363
2896
|
category: "templates",
|
|
2364
2897
|
scope: "read",
|
|
2365
2898
|
description: "List all prepaid package templates",
|
|
2366
|
-
params: {
|
|
2367
|
-
|
|
2368
|
-
},
|
|
2899
|
+
params: {},
|
|
2900
|
+
params_note: "No account filter. OCS listPrepaidPackageTemplate accepts only locationZoneId, templateId, destinationListId, resellerId, sponsorId. The former `accountId` param was rejected by OCS on every call and is not a rename of any of those five: accounts are a different id space from resellers and sponsors.",
|
|
2369
2901
|
response: {},
|
|
2370
2902
|
annotations: "readOnlyHint",
|
|
2371
2903
|
verified_against_server: true,
|
|
@@ -3499,26 +4031,9 @@ async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
|
3499
4031
|
{ accountId: acctId }
|
|
3500
4032
|
);
|
|
3501
4033
|
if (subResult.error) return { data: null, error: subResult.error };
|
|
3502
|
-
|
|
3503
|
-
const list = Array.isArray(raw) ? raw : raw?.subscriberList ?? [];
|
|
3504
|
-
aggregated.push(...list);
|
|
4034
|
+
aggregated.push(...subscriberRows(subResult.data));
|
|
3505
4035
|
}
|
|
3506
|
-
|
|
3507
|
-
return { data: active, error: null };
|
|
3508
|
-
}
|
|
3509
|
-
function formatBytes(bytes) {
|
|
3510
|
-
if (bytes === 0) return "0 B";
|
|
3511
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
3512
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
3513
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
3514
|
-
}
|
|
3515
|
-
function daysUntil(dateStr) {
|
|
3516
|
-
const now = /* @__PURE__ */ new Date();
|
|
3517
|
-
const target = new Date(dateStr);
|
|
3518
|
-
return Math.ceil((target.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
|
|
3519
|
-
}
|
|
3520
|
-
function toISODate(d) {
|
|
3521
|
-
return d.toISOString().split("T")[0];
|
|
4036
|
+
return { data: filterActiveSubscribers(aggregated), error: null };
|
|
3522
4037
|
}
|
|
3523
4038
|
function result(text, isError = false) {
|
|
3524
4039
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
@@ -3598,10 +4113,10 @@ function registerIntelligenceTools(server2, ctx) {
|
|
|
3598
4113
|
}
|
|
3599
4114
|
}
|
|
3600
4115
|
const now = /* @__PURE__ */ new Date();
|
|
3601
|
-
const
|
|
4116
|
+
const eventWindow = lastNDaysPeriod(3, now);
|
|
3602
4117
|
const events = await safeCall(ctx.env, token2, "subscriberNetworkEventsOverPeriod", {
|
|
3603
4118
|
subscriber: { iccid },
|
|
3604
|
-
period:
|
|
4119
|
+
period: eventWindow
|
|
3605
4120
|
});
|
|
3606
4121
|
if (events.data && Array.isArray(events.data)) {
|
|
3607
4122
|
if (events.data.length === 0) {
|
|
@@ -3778,11 +4293,11 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3778
4293
|
}, async ({ iccid }) => {
|
|
3779
4294
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3780
4295
|
const now = /* @__PURE__ */ new Date();
|
|
3781
|
-
const
|
|
4296
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3782
4297
|
const [usageResult, pkgResult] = await Promise.all([
|
|
3783
4298
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3784
4299
|
subscriber: { iccid },
|
|
3785
|
-
period:
|
|
4300
|
+
period: usagePeriod
|
|
3786
4301
|
}),
|
|
3787
4302
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
|
|
3788
4303
|
]);
|
|
@@ -3790,13 +4305,8 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3790
4305
|
const sections = [`# Usage Anomaly Report: ${iccid}
|
|
3791
4306
|
`];
|
|
3792
4307
|
const anomalies = [];
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
for (const entry of usageResult.data) {
|
|
3796
|
-
const bytes = Number(entry.dataBytes ?? entry.dataVolume ?? entry.totalData ?? 0);
|
|
3797
|
-
const date = String(entry.date ?? entry.day ?? "?");
|
|
3798
|
-
dailyData.push({ date, bytes });
|
|
3799
|
-
}
|
|
4308
|
+
const dailyData = extractDailyUsage(usageResult.data);
|
|
4309
|
+
if (dailyData.length > 0) {
|
|
3800
4310
|
if (dailyData.length >= 2) {
|
|
3801
4311
|
const volumes = dailyData.map((d) => d.bytes);
|
|
3802
4312
|
const mean = volumes.reduce((a, b) => a + b, 0) / volumes.length;
|
|
@@ -3864,24 +4374,21 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3864
4374
|
}, async ({ iccid }) => {
|
|
3865
4375
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3866
4376
|
const now = /* @__PURE__ */ new Date();
|
|
3867
|
-
const
|
|
4377
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3868
4378
|
const [usageResult, pkgResult, templatesResult] = await Promise.all([
|
|
3869
4379
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3870
4380
|
subscriber: { iccid },
|
|
3871
|
-
period:
|
|
4381
|
+
period: usagePeriod
|
|
3872
4382
|
}),
|
|
3873
4383
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
3874
4384
|
safeCall(ctx.env, token2, "listPrepaidPackageTemplate", {})
|
|
3875
4385
|
]);
|
|
3876
4386
|
const sections = [`# Package Optimization: ${iccid}
|
|
3877
4387
|
`];
|
|
4388
|
+
const optimizeRows = extractDailyUsage(usageResult.data);
|
|
3878
4389
|
let avgDailyData = 0;
|
|
3879
|
-
if (
|
|
3880
|
-
|
|
3881
|
-
(sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
|
|
3882
|
-
0
|
|
3883
|
-
);
|
|
3884
|
-
avgDailyData = totalData / usageResult.data.length;
|
|
4390
|
+
if (optimizeRows.length > 0) {
|
|
4391
|
+
avgDailyData = optimizeRows.reduce((sum, r) => sum + r.bytes, 0) / optimizeRows.length;
|
|
3885
4392
|
sections.push(`## Current Usage Pattern`);
|
|
3886
4393
|
sections.push(`- Average daily data: ${formatBytes(avgDailyData)}`);
|
|
3887
4394
|
sections.push(`- Projected monthly: ${formatBytes(avgDailyData * 30)}`);
|
|
@@ -3952,22 +4459,21 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3952
4459
|
}, async ({ iccid }) => {
|
|
3953
4460
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3954
4461
|
const now = /* @__PURE__ */ new Date();
|
|
3955
|
-
const
|
|
4462
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3956
4463
|
const [subResult, usageResult, pkgResult, activeResult] = await Promise.all([
|
|
3957
4464
|
safeCall(ctx.env, token2, "getSingleSubscriber", { iccid }),
|
|
3958
4465
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3959
4466
|
subscriber: { iccid },
|
|
3960
|
-
period:
|
|
4467
|
+
period: usagePeriod
|
|
3961
4468
|
}),
|
|
3962
4469
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
3963
4470
|
safeCall(ctx.env, token2, "getSubscriberActivePeriod", { iccid })
|
|
3964
4471
|
]);
|
|
3965
4472
|
let riskScore = 0;
|
|
3966
4473
|
const factors = [];
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
);
|
|
4474
|
+
const churnRows = extractDailyUsage(usageResult.data);
|
|
4475
|
+
if (churnRows.length >= 3) {
|
|
4476
|
+
const volumes = churnRows.map((r) => r.bytes);
|
|
3971
4477
|
const firstHalf = volumes.slice(0, Math.floor(volumes.length / 2));
|
|
3972
4478
|
const secondHalf = volumes.slice(Math.floor(volumes.length / 2));
|
|
3973
4479
|
const avgFirst = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
|
|
@@ -3984,7 +4490,7 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3984
4490
|
factors.push({ factor: "Moderately declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}%` });
|
|
3985
4491
|
}
|
|
3986
4492
|
}
|
|
3987
|
-
} else if (
|
|
4493
|
+
} else if (churnRows.length === 0) {
|
|
3988
4494
|
riskScore += 25;
|
|
3989
4495
|
factors.push({ factor: "No recent usage", impact: 25, detail: "Zero data activity in last 7 days" });
|
|
3990
4496
|
}
|
|
@@ -4279,7 +4785,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4279
4785
|
const sections = [`# High Cost Subscriber Report
|
|
4280
4786
|
`];
|
|
4281
4787
|
const now = /* @__PURE__ */ new Date();
|
|
4282
|
-
const
|
|
4788
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
4283
4789
|
const highCostSubs = [];
|
|
4284
4790
|
const batchSize = 5;
|
|
4285
4791
|
const subs = subsResult.data.slice(0, sampleSize);
|
|
@@ -4291,19 +4797,13 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4291
4797
|
const [usage, pkgs, loc] = await Promise.all([
|
|
4292
4798
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
4293
4799
|
subscriber: { iccid },
|
|
4294
|
-
period:
|
|
4800
|
+
period: usagePeriod
|
|
4295
4801
|
}),
|
|
4296
4802
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
4297
4803
|
safeCall(ctx.env, token2, "getSubscriberLocation", { iccid })
|
|
4298
4804
|
]);
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
const totalBytes = usage.data.reduce(
|
|
4302
|
-
(sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
|
|
4303
|
-
0
|
|
4304
|
-
);
|
|
4305
|
-
dailyAvgBytes = totalBytes / usage.data.length;
|
|
4306
|
-
}
|
|
4805
|
+
const dailyRows = extractDailyUsage(usage.data);
|
|
4806
|
+
const dailyAvgBytes = dailyRows.length > 0 ? dailyRows.reduce((sum, r) => sum + r.bytes, 0) / dailyRows.length : 0;
|
|
4307
4807
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
4308
4808
|
const activePkg = pkgs.data.find(
|
|
4309
4809
|
(p) => String(p.status ?? "").toUpperCase() === "ACTIVE"
|
|
@@ -4378,7 +4878,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4378
4878
|
});
|
|
4379
4879
|
server2.registerTool("detect_country_entry", {
|
|
4380
4880
|
title: "Detect Country Entry",
|
|
4381
|
-
description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
|
|
4881
|
+
description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). NOT AN AI TOOL: this is a deterministic MCC lookup and returns no model-written analysis, unlike the eight intelligence composites. COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
|
|
4382
4882
|
inputSchema: {
|
|
4383
4883
|
subscriber: z2.union([
|
|
4384
4884
|
z2.object({ subscriberId: z2.number() }).describe("Internal subscriber ID"),
|
|
@@ -4851,124 +5351,220 @@ import { z as z11 } from "zod";
|
|
|
4851
5351
|
// src/tools-ui-agent.ts
|
|
4852
5352
|
import { z as z4 } from "zod";
|
|
4853
5353
|
|
|
4854
|
-
// src/
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
};
|
|
4862
|
-
if (orgId) {
|
|
4863
|
-
try {
|
|
4864
|
-
const res = await fetch(`${baseUrl2}/organizations/${orgId}`, { headers });
|
|
4865
|
-
if (res.ok) {
|
|
4866
|
-
const org = await res.json();
|
|
4867
|
-
if (org.private_metadata?.ocs_portal?.username && org.private_metadata.ocs_portal.password) {
|
|
4868
|
-
return org.private_metadata.ocs_portal;
|
|
4869
|
-
}
|
|
4870
|
-
}
|
|
4871
|
-
} catch {
|
|
4872
|
-
}
|
|
4873
|
-
}
|
|
4874
|
-
if (userId) {
|
|
4875
|
-
try {
|
|
4876
|
-
const res = await fetch(`${baseUrl2}/users/${userId}`, { headers });
|
|
4877
|
-
if (res.ok) {
|
|
4878
|
-
const user = await res.json();
|
|
4879
|
-
if (user.private_metadata?.ocs_portal?.username && user.private_metadata.ocs_portal.password) {
|
|
4880
|
-
return user.private_metadata.ocs_portal;
|
|
4881
|
-
}
|
|
4882
|
-
}
|
|
4883
|
-
} catch {
|
|
4884
|
-
}
|
|
4885
|
-
}
|
|
4886
|
-
return null;
|
|
4887
|
-
}
|
|
4888
|
-
|
|
4889
|
-
// src/manus-common.ts
|
|
4890
|
-
var MANUS_API_BASE = "https://api.manus.ai/v2";
|
|
4891
|
-
|
|
4892
|
-
// src/tools-ui-agent.ts
|
|
4893
|
-
async function createManusTask(apiKey, prompt, title, outputSchema) {
|
|
4894
|
-
const body2 = {
|
|
4895
|
-
message: { content: prompt },
|
|
4896
|
-
agent_profile: "manus-1.6-lite",
|
|
4897
|
-
hide_in_task_list: true,
|
|
4898
|
-
interactive_mode: false,
|
|
4899
|
-
title
|
|
4900
|
-
};
|
|
4901
|
-
if (outputSchema) {
|
|
4902
|
-
body2.structured_output_schema = outputSchema;
|
|
4903
|
-
}
|
|
4904
|
-
const res = await fetch(`${MANUS_API_BASE}/task.create`, {
|
|
4905
|
-
method: "POST",
|
|
4906
|
-
headers: {
|
|
4907
|
-
"x-manus-api-key": apiKey,
|
|
4908
|
-
"Content-Type": "application/json"
|
|
4909
|
-
},
|
|
4910
|
-
body: JSON.stringify(body2)
|
|
4911
|
-
});
|
|
4912
|
-
return await res.json();
|
|
4913
|
-
}
|
|
4914
|
-
var OCS_DASHBOARD_DEFAULT = "https://ocs.esimvault.cloud";
|
|
4915
|
-
function noCredentialsError() {
|
|
4916
|
-
return {
|
|
4917
|
-
isError: true,
|
|
4918
|
-
content: [
|
|
4919
|
-
{
|
|
4920
|
-
type: "text",
|
|
4921
|
-
text: JSON.stringify({
|
|
4922
|
-
error: "ocs_portal_not_linked",
|
|
4923
|
-
message: "OCS portal credentials are not configured. An organisation admin must link their OCS portal login via the Carrier Console settings page (Organisation Profile \u2192 OCS Portal) before UI-agent operations can be dispatched.",
|
|
4924
|
-
action: "Navigate to https://console.carrier.llc/settings and link your OCS portal credentials."
|
|
4925
|
-
})
|
|
4926
|
-
}
|
|
4927
|
-
]
|
|
4928
|
-
};
|
|
4929
|
-
}
|
|
4930
|
-
function noManusKeyError() {
|
|
5354
|
+
// src/ui-agent-stub.ts
|
|
5355
|
+
var REMOTE_MCP_URL = "https://mcp.carrier.llc/mcp";
|
|
5356
|
+
var REASON_TEXT = {
|
|
5357
|
+
ocs_portal_credentials: "drives the OCS web dashboard, which needs OCS portal login credentials. Those are stored in Clerk private metadata and read through the Clerk Backend API, which the local CLI has no secret for.",
|
|
5358
|
+
recurring_runtime: "needs a recurring runtime. Browser Use Cloud has no native scheduler, so the hosted Worker implements schedules with KV plus a Cloudflare Cron Trigger and leader election. A CLI process exits when the client disconnects, so a schedule created here would never fire."
|
|
5359
|
+
};
|
|
5360
|
+
function uiAgentStubError(toolName, reason) {
|
|
4931
5361
|
return {
|
|
4932
5362
|
isError: true,
|
|
4933
5363
|
content: [
|
|
4934
5364
|
{
|
|
4935
5365
|
type: "text",
|
|
4936
5366
|
text: JSON.stringify({
|
|
4937
|
-
error: "
|
|
4938
|
-
|
|
5367
|
+
error: "requires_worker_runtime",
|
|
5368
|
+
tool: toolName,
|
|
5369
|
+
reason,
|
|
5370
|
+
message: `${toolName} ${REASON_TEXT[reason]} Connect to the remote Carrier MCP Worker to use this tool. Generic web browsing (ui_agent_ask / ui_agent_status) does work locally \u2014 set BROWSER_USE_API_KEY.`,
|
|
5371
|
+
remote_url: REMOTE_MCP_URL
|
|
4939
5372
|
})
|
|
4940
5373
|
}
|
|
4941
5374
|
]
|
|
4942
5375
|
};
|
|
4943
5376
|
}
|
|
4944
|
-
function buildLoginPreamble(dashboardUrl) {
|
|
4945
|
-
return `STEP 1 \u2014 LOGIN:
|
|
4946
|
-
Navigate to ${dashboardUrl}/login (or the main page if no /login path).
|
|
4947
|
-
Enter the OCS portal username and password provided below.
|
|
4948
|
-
Wait for the dashboard to fully load after login.
|
|
4949
|
-
If already logged in (session cookie persists), skip to STEP 2.
|
|
4950
5377
|
|
|
4951
|
-
|
|
5378
|
+
// src/portal-credentials.ts
|
|
5379
|
+
var PORTAL_OPT_IN_ENV = "CARRIER_ALLOW_PORTAL_PASSWORD";
|
|
5380
|
+
var PORTAL_USERNAME_ENV = "CARRIER_OCS_PORTAL_USERNAME";
|
|
5381
|
+
var PORTAL_PASSWORD_ENV = "CARRIER_OCS_PORTAL_PASSWORD";
|
|
5382
|
+
var PORTAL_DASHBOARD_ENV = "CARRIER_OCS_DASHBOARD_URL";
|
|
5383
|
+
var PORTAL_PASSWORD_WARNING = [
|
|
5384
|
+
`${PORTAL_OPT_IN_ENV} is set: the ten OCS-dashboard tools will use the portal login in`,
|
|
5385
|
+
`${PORTAL_USERNAME_ENV} / ${PORTAL_PASSWORD_ENV}.`,
|
|
5386
|
+
"",
|
|
5387
|
+
"That password is full dashboard access, not a scoped API token. Held in an environment",
|
|
5388
|
+
"variable it is readable from shell history, from `ps` output by any process on this",
|
|
5389
|
+
"machine, and from CI logs if this runs in CI. Carrier never logs it, returns it in tool",
|
|
5390
|
+
"output, or writes it to an audit row, but it is sent to Browser Use Cloud, which types it",
|
|
5391
|
+
"into the OCS login form.",
|
|
5392
|
+
"",
|
|
5393
|
+
"Each dashboard run also costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY.",
|
|
5394
|
+
"",
|
|
5395
|
+
`Unset ${PORTAL_OPT_IN_ENV} to turn this off; the tools then refuse as they did before.`
|
|
5396
|
+
].join("\n");
|
|
5397
|
+
var PORTAL_PASSWORD_RUN_NOTE = `This run used the OCS portal password from ${PORTAL_PASSWORD_ENV}, enabled by ${PORTAL_OPT_IN_ENV}. That credential is full dashboard access and was sent to Browser Use Cloud to type into the OCS login form. It is not logged, audited or returned anywhere in this response.`;
|
|
5398
|
+
function flagSet(raw) {
|
|
5399
|
+
if (raw === void 0) return false;
|
|
5400
|
+
const v = raw.trim().toLowerCase();
|
|
5401
|
+
return v === "1" || v === "true";
|
|
5402
|
+
}
|
|
5403
|
+
function nonEmpty(raw) {
|
|
5404
|
+
if (raw === void 0) return void 0;
|
|
5405
|
+
return raw.length > 0 ? raw : void 0;
|
|
5406
|
+
}
|
|
5407
|
+
function readPortalOptIn(env = process.env) {
|
|
5408
|
+
const flag = flagSet(env[PORTAL_OPT_IN_ENV]);
|
|
5409
|
+
const username = nonEmpty(env[PORTAL_USERNAME_ENV]);
|
|
5410
|
+
const password = nonEmpty(env[PORTAL_PASSWORD_ENV]);
|
|
5411
|
+
const hasCredentials = username !== void 0 && password !== void 0;
|
|
5412
|
+
if (flag && hasCredentials) {
|
|
5413
|
+
return {
|
|
5414
|
+
enabled: true,
|
|
5415
|
+
credentials: { username, password },
|
|
5416
|
+
...nonEmpty(env[PORTAL_DASHBOARD_ENV]) ? { dashboardUrl: env[PORTAL_DASHBOARD_ENV] } : {}
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
if (flag) return { enabled: false, gap: "flag_without_credentials" };
|
|
5420
|
+
if (hasCredentials)
|
|
5421
|
+
return { enabled: false, gap: "credentials_without_flag" };
|
|
5422
|
+
return { enabled: false, gap: "not_requested" };
|
|
4952
5423
|
}
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
5424
|
+
function warnPortalOptIn(optIn, write = (line) => console.error(line)) {
|
|
5425
|
+
if (optIn.enabled) {
|
|
5426
|
+
write(PORTAL_PASSWORD_WARNING);
|
|
5427
|
+
return;
|
|
5428
|
+
}
|
|
5429
|
+
if (optIn.gap === "flag_without_credentials") {
|
|
5430
|
+
write(PORTAL_PASSWORD_WARNING);
|
|
5431
|
+
write(
|
|
5432
|
+
`${PORTAL_USERNAME_ENV} and ${PORTAL_PASSWORD_ENV} are not both set, so the OCS-dashboard tools stay disabled.`
|
|
5433
|
+
);
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
if (optIn.gap === "credentials_without_flag") {
|
|
5437
|
+
write(
|
|
5438
|
+
`${PORTAL_PASSWORD_ENV} is set but ${PORTAL_OPT_IN_ENV} is not, so the OCS-dashboard tools stay disabled and this portal password is unused. Unset it unless you mean to opt in.`
|
|
5439
|
+
);
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
// src/ui-agent-runtime.ts
|
|
5444
|
+
function createMemoryStore() {
|
|
5445
|
+
const map = /* @__PURE__ */ new Map();
|
|
5446
|
+
return {
|
|
5447
|
+
async get(key) {
|
|
5448
|
+
const hit = map.get(key);
|
|
5449
|
+
if (!hit) return null;
|
|
5450
|
+
if (hit.expiresAt !== null && Date.now() >= hit.expiresAt) {
|
|
5451
|
+
map.delete(key);
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
return hit.value;
|
|
5455
|
+
},
|
|
5456
|
+
async put(key, value, options) {
|
|
5457
|
+
const ttl = options?.expirationTtl;
|
|
5458
|
+
map.set(key, {
|
|
5459
|
+
value,
|
|
5460
|
+
expiresAt: ttl === void 0 ? null : Date.now() + ttl * 1e3
|
|
5461
|
+
});
|
|
5462
|
+
},
|
|
5463
|
+
async delete(key) {
|
|
5464
|
+
map.delete(key);
|
|
5465
|
+
}
|
|
5466
|
+
};
|
|
5467
|
+
}
|
|
5468
|
+
var DEFAULT_MAX_RUNS_PER_PROCESS = 10;
|
|
5469
|
+
var MAX_RUNS_ENV = "CARRIER_BROWSER_USE_MAX_RUNS";
|
|
5470
|
+
var SpendLimitError = class extends Error {
|
|
5471
|
+
constructor(limit) {
|
|
5472
|
+
super(
|
|
5473
|
+
`Browser-agent run limit reached: ${limit} run(s) in this CLI session. Each run costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY. Raise it with ${MAX_RUNS_ENV}=<n>, or 0 to disable the browser agent entirely. Restarting the CLI resets the count.`
|
|
5474
|
+
);
|
|
5475
|
+
this.limit = limit;
|
|
5476
|
+
this.name = "SpendLimitError";
|
|
5477
|
+
}
|
|
5478
|
+
limit;
|
|
5479
|
+
};
|
|
5480
|
+
function maxRunsPerProcess(env = process.env) {
|
|
5481
|
+
const raw = env[MAX_RUNS_ENV];
|
|
5482
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
5483
|
+
return DEFAULT_MAX_RUNS_PER_PROCESS;
|
|
5484
|
+
}
|
|
5485
|
+
const parsed = Number(raw);
|
|
5486
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
5487
|
+
return DEFAULT_MAX_RUNS_PER_PROCESS;
|
|
5488
|
+
}
|
|
5489
|
+
return parsed;
|
|
5490
|
+
}
|
|
5491
|
+
var RunBudget = class {
|
|
5492
|
+
constructor(limit) {
|
|
5493
|
+
this.limit = limit;
|
|
5494
|
+
}
|
|
5495
|
+
limit;
|
|
5496
|
+
used = 0;
|
|
5497
|
+
/** Consume one run, or throw. Call immediately before dispatching. */
|
|
5498
|
+
reserve() {
|
|
5499
|
+
if (this.used >= this.limit) throw new SpendLimitError(this.limit);
|
|
5500
|
+
this.used += 1;
|
|
5501
|
+
}
|
|
5502
|
+
get remaining() {
|
|
5503
|
+
return Math.max(0, this.limit - this.used);
|
|
5504
|
+
}
|
|
5505
|
+
get spent() {
|
|
5506
|
+
return this.used;
|
|
5507
|
+
}
|
|
5508
|
+
};
|
|
5509
|
+
function createUiAgentRuntime(processEnv = process.env) {
|
|
5510
|
+
const apiKey2 = processEnv.BROWSER_USE_API_KEY;
|
|
5511
|
+
const spend = { runs: 0, costUsd: 0 };
|
|
5512
|
+
const audit2 = (row) => {
|
|
5513
|
+
const cost = row.doubles?.[1];
|
|
5514
|
+
if (typeof cost === "number" && cost > 0) {
|
|
5515
|
+
spend.runs += 1;
|
|
5516
|
+
spend.costUsd = Math.round((spend.costUsd + cost) * 1e6) / 1e6;
|
|
5517
|
+
}
|
|
5518
|
+
};
|
|
5519
|
+
return {
|
|
5520
|
+
env: {
|
|
5521
|
+
...apiKey2 ? { apiKey: apiKey2 } : {},
|
|
5522
|
+
store: createMemoryStore(),
|
|
5523
|
+
audit: audit2
|
|
5524
|
+
},
|
|
5525
|
+
budget: new RunBudget(maxRunsPerProcess(processEnv)),
|
|
5526
|
+
spend,
|
|
5527
|
+
configured: Boolean(apiKey2),
|
|
5528
|
+
dispatchedTaskIds: /* @__PURE__ */ new Set()
|
|
5529
|
+
};
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
// src/tools-ui-agent.ts
|
|
5533
|
+
var UI_AGENT_TOOL_SCOPES = {
|
|
5534
|
+
ui_create_steering_list: "write",
|
|
5535
|
+
ui_build_steering_list: "write",
|
|
5536
|
+
ui_set_account_steering_list: "write",
|
|
5537
|
+
ui_request_reseller_relay_change: "write",
|
|
5538
|
+
ui_create_account: "admin",
|
|
5539
|
+
ui_create_destination_list: "write",
|
|
5540
|
+
ui_edit_destination_list: "write",
|
|
5541
|
+
ui_delete_destination_list: "admin",
|
|
5542
|
+
ui_delete_package_template: "admin",
|
|
5543
|
+
ui_edit_location_zone: "write",
|
|
5544
|
+
ui_delete_location_zone: "admin"
|
|
4963
5545
|
};
|
|
4964
|
-
|
|
5546
|
+
var STDIO_NOTE_DISABLED = " Requires the remote Carrier MCP Worker deployment \u2014 the local CLI holds no OCS portal credentials.";
|
|
5547
|
+
var STDIO_NOTE_ENABLED = " Runs locally: CARRIER_ALLOW_PORTAL_PASSWORD is set and OCS portal credentials are supplied, so this drives the dashboard from this machine. Each run costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY and counts against CARRIER_BROWSER_USE_MAX_RUNS.";
|
|
5548
|
+
function stdioNote(optIn) {
|
|
5549
|
+
return optIn.enabled ? STDIO_NOTE_ENABLED : STDIO_NOTE_DISABLED;
|
|
5550
|
+
}
|
|
5551
|
+
function portalStub(toolName) {
|
|
5552
|
+
return uiAgentStubError(toolName, "ocs_portal_credentials");
|
|
5553
|
+
}
|
|
5554
|
+
function jsonResult(body2, isError = false) {
|
|
5555
|
+
return {
|
|
5556
|
+
...isError ? { isError: true } : {},
|
|
5557
|
+
content: [{ type: "text", text: JSON.stringify(body2, null, 2) }]
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5560
|
+
function wrapPortalHandler(toolName, gapId, requiredScope, ctx, runtime, optIn, buildPrompt) {
|
|
4965
5561
|
return async (args) => {
|
|
4966
5562
|
const start = Date.now();
|
|
4967
|
-
const
|
|
5563
|
+
const ocsMethod = `[ui-agent:${gapId}]`;
|
|
4968
5564
|
if (!ctx.props.scope.includes(requiredScope)) {
|
|
4969
5565
|
ctx.audit({
|
|
4970
5566
|
tool_name: toolName,
|
|
4971
|
-
ocs_method:
|
|
5567
|
+
ocs_method: ocsMethod,
|
|
4972
5568
|
status: "scope_denied",
|
|
4973
5569
|
dry_run: false,
|
|
4974
5570
|
duration_ms: 0
|
|
@@ -4983,114 +5579,130 @@ function wrapUiAgentHandler(toolName, gapId, requiredScope, ctx, buildPrompt) {
|
|
|
4983
5579
|
]
|
|
4984
5580
|
};
|
|
4985
5581
|
}
|
|
4986
|
-
if (!
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
5582
|
+
if (!optIn.enabled) return portalStub(toolName);
|
|
5583
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
5584
|
+
return jsonResult(
|
|
5585
|
+
{
|
|
5586
|
+
error: "browser_use_not_configured",
|
|
5587
|
+
tool: toolName,
|
|
5588
|
+
message: "BROWSER_USE_API_KEY is not set, so the dashboard agent cannot run. Set it in the environment of the process running this MCP server. Runs are billed to that key at roughly $0.20-1.00 each."
|
|
5589
|
+
},
|
|
5590
|
+
true
|
|
5591
|
+
);
|
|
4993
5592
|
}
|
|
4994
|
-
const
|
|
4995
|
-
const
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
` + prompt + `
|
|
5004
|
-
|
|
5005
|
-
STEP 3 \u2014 VERIFICATION:
|
|
5006
|
-
After completing the operation, verify the result by checking the dashboard shows the expected state.
|
|
5007
|
-
Take a screenshot of the final state for audit purposes.
|
|
5008
|
-
Report success or failure with a clear summary.`;
|
|
5009
|
-
if (isDryRun) {
|
|
5010
|
-
const redactedPrompt = fullPrompt.replaceAll(creds.password, "***REDACTED***").replaceAll(creds.username, "***REDACTED***");
|
|
5593
|
+
const { credentials } = optIn;
|
|
5594
|
+
const dashboardUrl = optIn.dashboardUrl ?? OCS_DASHBOARD_DEFAULT;
|
|
5595
|
+
const redact = (text) => redactCredentials(text, credentials);
|
|
5596
|
+
const fullPrompt = assembleUiAgentPrompt(
|
|
5597
|
+
buildPrompt(args, dashboardUrl),
|
|
5598
|
+
credentials,
|
|
5599
|
+
dashboardUrl
|
|
5600
|
+
);
|
|
5601
|
+
if (args.dry_run === true) {
|
|
5011
5602
|
ctx.audit({
|
|
5012
5603
|
tool_name: toolName,
|
|
5013
|
-
ocs_method:
|
|
5604
|
+
ocs_method: ocsMethod,
|
|
5014
5605
|
status: "dry_run",
|
|
5015
5606
|
dry_run: true,
|
|
5016
5607
|
duration_ms: 0,
|
|
5017
5608
|
event_type: "ui_agent_dispatch"
|
|
5018
5609
|
});
|
|
5019
|
-
return {
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5610
|
+
return jsonResult({
|
|
5611
|
+
dry_run: true,
|
|
5612
|
+
tool: toolName,
|
|
5613
|
+
gap_id: gapId,
|
|
5614
|
+
agent_prompt_preview: redact(fullPrompt),
|
|
5615
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
5616
|
+
note: "No browser agent was dispatched. Set dry_run=false to execute.",
|
|
5617
|
+
portal_password_note: PORTAL_PASSWORD_RUN_NOTE
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
try {
|
|
5621
|
+
runtime.budget.reserve();
|
|
5622
|
+
} catch (err7) {
|
|
5623
|
+
if (err7 instanceof SpendLimitError) {
|
|
5624
|
+
ctx.audit({
|
|
5625
|
+
tool_name: toolName,
|
|
5626
|
+
ocs_method: ocsMethod,
|
|
5627
|
+
// The existing quota status, not a new one: AuditRow's status is a
|
|
5628
|
+
// closed union shared with the Worker.
|
|
5629
|
+
status: "quota_exceeded",
|
|
5630
|
+
dry_run: false,
|
|
5631
|
+
duration_ms: 0,
|
|
5632
|
+
event_type: "ui_agent_dispatch"
|
|
5633
|
+
});
|
|
5634
|
+
return jsonResult(
|
|
5635
|
+
{ error: "run_limit_reached", message: err7.message },
|
|
5636
|
+
true
|
|
5637
|
+
);
|
|
5638
|
+
}
|
|
5639
|
+
throw err7;
|
|
5033
5640
|
}
|
|
5034
5641
|
try {
|
|
5035
|
-
const result2 = await
|
|
5036
|
-
ctx.env.MANUS_API_KEY,
|
|
5642
|
+
const result2 = await runUiAgent(
|
|
5037
5643
|
fullPrompt,
|
|
5038
|
-
`Carrier
|
|
5039
|
-
|
|
5644
|
+
`Carrier CLI UI Agent: ${toolName} (${gapId})`,
|
|
5645
|
+
runtime.env,
|
|
5646
|
+
{ ownerSub: ctx.props.sub, redact }
|
|
5040
5647
|
);
|
|
5041
|
-
if (!result2.
|
|
5648
|
+
if (!result2.task_id) {
|
|
5042
5649
|
ctx.audit({
|
|
5043
5650
|
tool_name: toolName,
|
|
5044
|
-
ocs_method:
|
|
5651
|
+
ocs_method: ocsMethod,
|
|
5045
5652
|
status: "error",
|
|
5046
5653
|
dry_run: false,
|
|
5047
5654
|
duration_ms: Date.now() - start,
|
|
5048
5655
|
event_type: "ui_agent_dispatch"
|
|
5049
5656
|
});
|
|
5050
|
-
return
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
message: result2.error?.message ?? "Failed to create Manus task",
|
|
5058
|
-
code: result2.error?.code
|
|
5059
|
-
})
|
|
5060
|
-
}
|
|
5061
|
-
]
|
|
5062
|
-
};
|
|
5657
|
+
return jsonResult(
|
|
5658
|
+
{
|
|
5659
|
+
error: "steel_dispatch_failed",
|
|
5660
|
+
message: "Failed to create a browser agent task"
|
|
5661
|
+
},
|
|
5662
|
+
true
|
|
5663
|
+
);
|
|
5063
5664
|
}
|
|
5665
|
+
runtime.dispatchedTaskIds.add(result2.task_id);
|
|
5064
5666
|
ctx.audit({
|
|
5065
5667
|
tool_name: toolName,
|
|
5066
|
-
ocs_method:
|
|
5067
|
-
status: "ui_agent_dispatched",
|
|
5668
|
+
ocs_method: ocsMethod,
|
|
5669
|
+
status: result2.status === "error" ? "error" : "ui_agent_dispatched",
|
|
5068
5670
|
dry_run: false,
|
|
5069
5671
|
duration_ms: Date.now() - start,
|
|
5070
5672
|
event_type: "ui_agent_dispatch",
|
|
5071
5673
|
manus_task_id: result2.task_id
|
|
5674
|
+
// AE blob position is the contract; the name is history
|
|
5675
|
+
});
|
|
5676
|
+
return jsonResult({
|
|
5677
|
+
status: result2.status === "stopped" ? "ui_agent_completed" : result2.status === "waiting" ? "ui_agent_awaiting_input" : result2.status,
|
|
5678
|
+
tool: toolName,
|
|
5679
|
+
gap_id: gapId,
|
|
5680
|
+
// Held still across the vendor swap and across transports: MCP clients
|
|
5681
|
+
// and the console read these names.
|
|
5682
|
+
steel_task_id: result2.task_id,
|
|
5683
|
+
steel_session_viewer_url: result2.session_viewer_url,
|
|
5684
|
+
// Redacted defensively. `runUiAgent` already applies the same redactor
|
|
5685
|
+
// before the record is stored, so this is the second of two passes over
|
|
5686
|
+
// the fields an agent can echo its instructions into. Field-by-field
|
|
5687
|
+
// rather than over the serialised JSON: a password containing a quote
|
|
5688
|
+
// or a backslash is escaped by JSON.stringify and would no longer match
|
|
5689
|
+
// a plain substring replace.
|
|
5690
|
+
result: result2.result ? {
|
|
5691
|
+
...result2.result,
|
|
5692
|
+
summary: redact(result2.result.summary),
|
|
5693
|
+
...result2.result.entity_id !== void 0 ? { entity_id: redact(result2.result.entity_id) } : {},
|
|
5694
|
+
...result2.result.error_message !== void 0 ? { error_message: redact(result2.result.error_message) } : {}
|
|
5695
|
+
} : void 0,
|
|
5696
|
+
cost_usd: result2.spend?.costUsd,
|
|
5697
|
+
session_spend_usd: runtime.spend.costUsd,
|
|
5698
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
5699
|
+
note: "The run completed in-process \u2014 a local CLI has no background runtime, so this call blocked for its duration.",
|
|
5700
|
+
portal_password_note: PORTAL_PASSWORD_RUN_NOTE
|
|
5072
5701
|
});
|
|
5073
|
-
return {
|
|
5074
|
-
content: [
|
|
5075
|
-
{
|
|
5076
|
-
type: "text",
|
|
5077
|
-
text: JSON.stringify({
|
|
5078
|
-
status: "ui_agent_dispatched",
|
|
5079
|
-
tool: toolName,
|
|
5080
|
-
gap_id: gapId,
|
|
5081
|
-
manus_task_id: result2.task_id,
|
|
5082
|
-
manus_task_url: result2.task_url,
|
|
5083
|
-
note: "A Manus browser automation agent has been dispatched to perform this operation on the OCS web dashboard. The agent will log in, execute the operation, and verify the result. You can track progress at the task URL above. Real-time completion updates arrive via webhook and are recorded in the audit log. The poll_endpoint below is provided for backward compatibility.",
|
|
5084
|
-
webhook_status: "active",
|
|
5085
|
-
poll_endpoint: `GET ${MANUS_API_BASE}/task.listMessages?task_id=${result2.task_id}&order=desc&limit=5`
|
|
5086
|
-
}, null, 2)
|
|
5087
|
-
}
|
|
5088
|
-
]
|
|
5089
|
-
};
|
|
5090
5702
|
} catch (err7) {
|
|
5091
5703
|
ctx.audit({
|
|
5092
5704
|
tool_name: toolName,
|
|
5093
|
-
ocs_method:
|
|
5705
|
+
ocs_method: ocsMethod,
|
|
5094
5706
|
status: "error",
|
|
5095
5707
|
dry_run: false,
|
|
5096
5708
|
duration_ms: Date.now() - start,
|
|
@@ -5101,57 +5713,44 @@ Report success or failure with a clear summary.`;
|
|
|
5101
5713
|
content: [
|
|
5102
5714
|
{
|
|
5103
5715
|
type: "text",
|
|
5104
|
-
text:
|
|
5716
|
+
text: redact(
|
|
5717
|
+
`Error dispatching the UI agent: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
5718
|
+
)
|
|
5105
5719
|
}
|
|
5106
5720
|
]
|
|
5107
5721
|
};
|
|
5108
5722
|
}
|
|
5109
5723
|
};
|
|
5110
5724
|
}
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
ui_set_account_steering_list: "write",
|
|
5115
|
-
ui_request_reseller_relay_change: "write",
|
|
5116
|
-
ui_create_account: "admin",
|
|
5117
|
-
ui_create_destination_list: "write",
|
|
5118
|
-
ui_edit_destination_list: "write",
|
|
5119
|
-
ui_delete_destination_list: "admin",
|
|
5120
|
-
ui_delete_package_template: "admin",
|
|
5121
|
-
ui_edit_location_zone: "write",
|
|
5122
|
-
ui_delete_location_zone: "admin"
|
|
5123
|
-
};
|
|
5124
|
-
function registerAllUiAgentTools(server2, ctx) {
|
|
5725
|
+
function registerAllUiAgentTools(server2, ctx, runtime, optIn = readPortalOptIn()) {
|
|
5726
|
+
const NOTE = stdioNote(optIn);
|
|
5727
|
+
const gated = (toolName, gapId, scope, buildPrompt) => wrapPortalHandler(toolName, gapId, scope, ctx, runtime, optIn, buildPrompt);
|
|
5125
5728
|
server2.registerTool(
|
|
5126
5729
|
"ui_create_steering_list",
|
|
5127
5730
|
{
|
|
5128
5731
|
title: "Create Steering List (UI Agent)",
|
|
5129
|
-
description: "Creates a new network steering list (OPLMN preference configuration) via the OCS web dashboard. This operation is not available via the OCS REST API. A
|
|
5732
|
+
description: "Creates a new network steering list (OPLMN preference configuration) via the OCS web dashboard. This operation is not available via the OCS REST API. A browser agent is dispatched to perform the operation. Params: `name` (steering list name), `description` (optional)." + NOTE,
|
|
5130
5733
|
inputSchema: {
|
|
5131
5734
|
name: z4.string().describe("Name for the new steering list"),
|
|
5132
5735
|
description: z4.string().optional().describe("Optional description for the steering list"),
|
|
5133
5736
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5134
5737
|
}
|
|
5135
5738
|
},
|
|
5136
|
-
|
|
5739
|
+
gated(
|
|
5137
5740
|
"ui_create_steering_list",
|
|
5138
5741
|
"G-01",
|
|
5139
5742
|
"write",
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
` + (args.description ? ` Description: ${args.description}
|
|
5145
|
-
` : "") + `Click the "Create" or "Add" button to create the steering list.
|
|
5146
|
-
After creation, note the new steering list ID from the dashboard.
|
|
5147
|
-
`
|
|
5743
|
+
(args, dashboardUrl) => buildCreateSteeringListPrompt(
|
|
5744
|
+
args,
|
|
5745
|
+
dashboardUrl
|
|
5746
|
+
)
|
|
5148
5747
|
)
|
|
5149
5748
|
);
|
|
5150
5749
|
server2.registerTool(
|
|
5151
5750
|
"ui_build_steering_list",
|
|
5152
5751
|
{
|
|
5153
5752
|
title: "Build Steering List (UI Agent)",
|
|
5154
|
-
description: "Adds or removes operators (MCC-MNC) from an existing steering list via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `steering_list_id`, `add_operators` (array of MCC-MNC to add), `remove_operators` (array to remove), `operator_type` ('priority' or 'excluded').",
|
|
5753
|
+
description: "Adds or removes operators (MCC-MNC) from an existing steering list via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `steering_list_id`, `add_operators` (array of MCC-MNC to add), `remove_operators` (array to remove), `operator_type` ('priority' or 'excluded')." + NOTE,
|
|
5155
5754
|
inputSchema: {
|
|
5156
5755
|
steering_list_id: z4.number().describe("ID of the steering list to modify"),
|
|
5157
5756
|
add_operators: z4.array(z4.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
|
|
@@ -5160,42 +5759,35 @@ After creation, note the new steering list ID from the dashboard.
|
|
|
5160
5759
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5161
5760
|
}
|
|
5162
5761
|
},
|
|
5163
|
-
|
|
5762
|
+
gated(
|
|
5164
5763
|
"ui_build_steering_list",
|
|
5165
5764
|
"G-02",
|
|
5166
5765
|
"write",
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
` + (args.add_operators && args.add_operators.length > 0 ? `Add the following operators: ${args.add_operators.join(", ")}
|
|
5172
|
-
` : "") + (args.remove_operators && args.remove_operators.length > 0 ? `Remove the following operators: ${args.remove_operators.join(", ")}
|
|
5173
|
-
` : "") + `Save the changes and verify the updated operator list.
|
|
5174
|
-
`
|
|
5766
|
+
(args, dashboardUrl) => buildBuildSteeringListPrompt(
|
|
5767
|
+
args,
|
|
5768
|
+
dashboardUrl
|
|
5769
|
+
)
|
|
5175
5770
|
)
|
|
5176
5771
|
);
|
|
5177
5772
|
server2.registerTool(
|
|
5178
5773
|
"ui_set_account_steering_list",
|
|
5179
5774
|
{
|
|
5180
5775
|
title: "Set Account Steering List (UI Agent)",
|
|
5181
|
-
description: "Assigns or removes a steering list at the account level via the OCS web dashboard. The subscriber-level counterpart `modify_subscriber_steering_list` is available via API; this account-level operation is UI-only. Params: `account_id`, `steering_list_id` (0 to remove).",
|
|
5776
|
+
description: "Assigns or removes a steering list at the account level via the OCS web dashboard. The subscriber-level counterpart `modify_subscriber_steering_list` is available via API; this account-level operation is UI-only. Params: `account_id`, `steering_list_id` (0 to remove)." + NOTE,
|
|
5182
5777
|
inputSchema: {
|
|
5183
5778
|
account_id: z4.number().describe("Account ID to assign the steering list to"),
|
|
5184
5779
|
steering_list_id: z4.number().describe("Steering list ID to assign (0 to remove/unset)"),
|
|
5185
5780
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5186
5781
|
}
|
|
5187
5782
|
},
|
|
5188
|
-
|
|
5783
|
+
gated(
|
|
5189
5784
|
"ui_set_account_steering_list",
|
|
5190
5785
|
"G-03",
|
|
5191
5786
|
"write",
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
` : `Assign steering list ID ${args.steering_list_id} to this account.
|
|
5197
|
-
`) + `Save the changes and verify the steering list assignment is updated.
|
|
5198
|
-
`
|
|
5787
|
+
(args, dashboardUrl) => buildSetAccountSteeringListPrompt(
|
|
5788
|
+
args,
|
|
5789
|
+
dashboardUrl
|
|
5790
|
+
)
|
|
5199
5791
|
)
|
|
5200
5792
|
);
|
|
5201
5793
|
server2.registerTool(
|
|
@@ -5215,7 +5807,12 @@ Open account ID ${args.account_id}.
|
|
|
5215
5807
|
if (!ctx.props.scope.includes("write")) {
|
|
5216
5808
|
return {
|
|
5217
5809
|
isError: true,
|
|
5218
|
-
content: [
|
|
5810
|
+
content: [
|
|
5811
|
+
{
|
|
5812
|
+
type: "text",
|
|
5813
|
+
text: "Scope denied: tool 'ui_request_reseller_relay_change' requires 'write' scope."
|
|
5814
|
+
}
|
|
5815
|
+
]
|
|
5219
5816
|
};
|
|
5220
5817
|
}
|
|
5221
5818
|
const resellerId = args.reseller_id ?? ctx.props.reseller_id;
|
|
@@ -5227,7 +5824,15 @@ Open account ID ${args.account_id}.
|
|
|
5227
5824
|
if (requested.length === 0) {
|
|
5228
5825
|
return {
|
|
5229
5826
|
isError: true,
|
|
5230
|
-
content: [
|
|
5827
|
+
content: [
|
|
5828
|
+
{
|
|
5829
|
+
type: "text",
|
|
5830
|
+
text: JSON.stringify({
|
|
5831
|
+
error: "relay_state_required",
|
|
5832
|
+
message: "Choose at least one relay flag and desired state."
|
|
5833
|
+
})
|
|
5834
|
+
}
|
|
5835
|
+
]
|
|
5231
5836
|
};
|
|
5232
5837
|
}
|
|
5233
5838
|
const draft = [
|
|
@@ -5247,14 +5852,23 @@ Open account ID ${args.account_id}.
|
|
|
5247
5852
|
event_type: "ui_agent_dispatch"
|
|
5248
5853
|
});
|
|
5249
5854
|
return {
|
|
5250
|
-
content: [
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5855
|
+
content: [
|
|
5856
|
+
{
|
|
5857
|
+
type: "text",
|
|
5858
|
+
text: JSON.stringify(
|
|
5859
|
+
{
|
|
5860
|
+
status: "parent_request_drafted",
|
|
5861
|
+
capability: "parent_controlled",
|
|
5862
|
+
reseller_id: resellerId,
|
|
5863
|
+
parent_reseller: resellerId === 1170 ? "Bridge4IP" : "parent reseller/support",
|
|
5864
|
+
draft,
|
|
5865
|
+
sent: false
|
|
5866
|
+
},
|
|
5867
|
+
null,
|
|
5868
|
+
2
|
|
5869
|
+
)
|
|
5870
|
+
}
|
|
5871
|
+
]
|
|
5258
5872
|
};
|
|
5259
5873
|
}
|
|
5260
5874
|
);
|
|
@@ -5262,7 +5876,7 @@ Open account ID ${args.account_id}.
|
|
|
5262
5876
|
"ui_create_account",
|
|
5263
5877
|
{
|
|
5264
5878
|
title: "Create Account (UI Agent)",
|
|
5265
|
-
description: "Creates a new sub-account under the reseller via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `name` (account name), `description` (optional), `initial_balance` (optional, default 0).",
|
|
5879
|
+
description: "Creates a new sub-account under the reseller via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `name` (account name), `description` (optional), `initial_balance` (optional, default 0)." + NOTE,
|
|
5266
5880
|
inputSchema: {
|
|
5267
5881
|
name: z4.string().describe("Name for the new account"),
|
|
5268
5882
|
description: z4.string().optional().describe("Optional description"),
|
|
@@ -5270,26 +5884,21 @@ Open account ID ${args.account_id}.
|
|
|
5270
5884
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5271
5885
|
}
|
|
5272
5886
|
},
|
|
5273
|
-
|
|
5887
|
+
gated(
|
|
5274
5888
|
"ui_create_account",
|
|
5275
5889
|
"G-05",
|
|
5276
5890
|
"admin",
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
` + (args.description ? ` Description: ${args.description}
|
|
5282
|
-
` : "") + (args.initial_balance ? ` Initial balance: ${args.initial_balance}
|
|
5283
|
-
` : "") + `Click the "Create" or "Add" button.
|
|
5284
|
-
After creation, note the new account ID from the dashboard.
|
|
5285
|
-
`
|
|
5891
|
+
(args, dashboardUrl) => buildCreateAccountPrompt(
|
|
5892
|
+
args,
|
|
5893
|
+
dashboardUrl
|
|
5894
|
+
)
|
|
5286
5895
|
)
|
|
5287
5896
|
);
|
|
5288
5897
|
server2.registerTool(
|
|
5289
5898
|
"ui_create_destination_list",
|
|
5290
5899
|
{
|
|
5291
5900
|
title: "Create Destination List (UI Agent)",
|
|
5292
|
-
description: "Creates a new destination list (named set of phone number prefixes for MOC call permissions) via the OCS web dashboard. Params: `name`, `prefixes` (array of prefix strings), `description`.",
|
|
5901
|
+
description: "Creates a new destination list (named set of phone number prefixes for MOC call permissions) via the OCS web dashboard. Params: `name`, `prefixes` (array of prefix strings), `description`." + NOTE,
|
|
5293
5902
|
inputSchema: {
|
|
5294
5903
|
name: z4.string().describe("Name for the new destination list"),
|
|
5295
5904
|
prefixes: z4.array(z4.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
|
|
@@ -5297,25 +5906,21 @@ After creation, note the new account ID from the dashboard.
|
|
|
5297
5906
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5298
5907
|
}
|
|
5299
5908
|
},
|
|
5300
|
-
|
|
5909
|
+
gated(
|
|
5301
5910
|
"ui_create_destination_list",
|
|
5302
5911
|
"G-12",
|
|
5303
5912
|
"write",
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
` + (args.description ? ` Description: ${args.description}
|
|
5309
|
-
` : "") + (args.prefixes && args.prefixes.length > 0 ? ` Prefixes to add: ${args.prefixes.join(", ")}
|
|
5310
|
-
` : "") + `Save the new destination list and note the ID.
|
|
5311
|
-
`
|
|
5913
|
+
(args, dashboardUrl) => buildCreateDestinationListPrompt(
|
|
5914
|
+
args,
|
|
5915
|
+
dashboardUrl
|
|
5916
|
+
)
|
|
5312
5917
|
)
|
|
5313
5918
|
);
|
|
5314
5919
|
server2.registerTool(
|
|
5315
5920
|
"ui_edit_destination_list",
|
|
5316
5921
|
{
|
|
5317
5922
|
title: "Edit Destination List (UI Agent)",
|
|
5318
|
-
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`.",
|
|
5923
|
+
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`." + NOTE,
|
|
5319
5924
|
inputSchema: {
|
|
5320
5925
|
destination_list_id: z4.number().describe("ID of the destination list to edit"),
|
|
5321
5926
|
add_prefixes: z4.array(z4.string()).optional().describe("Prefixes to add"),
|
|
@@ -5324,69 +5929,61 @@ Create a new destination list with the following details:
|
|
|
5324
5929
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5325
5930
|
}
|
|
5326
5931
|
},
|
|
5327
|
-
|
|
5932
|
+
gated(
|
|
5328
5933
|
"ui_edit_destination_list",
|
|
5329
5934
|
"G-12",
|
|
5330
5935
|
"write",
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
` : "") + (args.add_prefixes && args.add_prefixes.length > 0 ? `Add prefixes: ${args.add_prefixes.join(", ")}
|
|
5336
|
-
` : "") + (args.remove_prefixes && args.remove_prefixes.length > 0 ? `Remove prefixes: ${args.remove_prefixes.join(", ")}
|
|
5337
|
-
` : "") + `Save the changes and verify the updated prefix list.
|
|
5338
|
-
`
|
|
5936
|
+
(args, dashboardUrl) => buildEditDestinationListPrompt(
|
|
5937
|
+
args,
|
|
5938
|
+
dashboardUrl
|
|
5939
|
+
)
|
|
5339
5940
|
)
|
|
5340
5941
|
);
|
|
5341
5942
|
server2.registerTool(
|
|
5342
5943
|
"ui_delete_destination_list",
|
|
5343
5944
|
{
|
|
5344
5945
|
title: "Delete Destination List (UI Agent)",
|
|
5345
|
-
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone.",
|
|
5946
|
+
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone." + NOTE,
|
|
5346
5947
|
inputSchema: {
|
|
5347
5948
|
destination_list_id: z4.number().describe("ID of the destination list to delete"),
|
|
5348
5949
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5349
5950
|
}
|
|
5350
5951
|
},
|
|
5351
|
-
|
|
5952
|
+
gated(
|
|
5352
5953
|
"ui_delete_destination_list",
|
|
5353
5954
|
"G-12",
|
|
5354
5955
|
"admin",
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
Verify the list no longer appears in the dashboard.
|
|
5360
|
-
`
|
|
5956
|
+
(args, dashboardUrl) => buildDeleteDestinationListPrompt(
|
|
5957
|
+
args,
|
|
5958
|
+
dashboardUrl
|
|
5959
|
+
)
|
|
5361
5960
|
)
|
|
5362
5961
|
);
|
|
5363
5962
|
server2.registerTool(
|
|
5364
5963
|
"ui_delete_package_template",
|
|
5365
5964
|
{
|
|
5366
5965
|
title: "Delete Package Template (UI Agent)",
|
|
5367
|
-
description: "Deletes a package template from the product catalog via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `template_id`. WARNING: This is destructive.",
|
|
5966
|
+
description: "Deletes a package template from the product catalog via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `template_id`. WARNING: This is destructive." + NOTE,
|
|
5368
5967
|
inputSchema: {
|
|
5369
5968
|
template_id: z4.number().describe("ID of the package template to delete"),
|
|
5370
5969
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5371
5970
|
}
|
|
5372
5971
|
},
|
|
5373
|
-
|
|
5972
|
+
gated(
|
|
5374
5973
|
"ui_delete_package_template",
|
|
5375
5974
|
"G-18",
|
|
5376
5975
|
"admin",
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
Verify the template no longer appears in the template list.
|
|
5382
|
-
`
|
|
5976
|
+
(args, dashboardUrl) => buildDeletePackageTemplatePrompt(
|
|
5977
|
+
args,
|
|
5978
|
+
dashboardUrl
|
|
5979
|
+
)
|
|
5383
5980
|
)
|
|
5384
5981
|
);
|
|
5385
5982
|
server2.registerTool(
|
|
5386
5983
|
"ui_edit_location_zone",
|
|
5387
5984
|
{
|
|
5388
5985
|
title: "Edit Location Zone (UI Agent)",
|
|
5389
|
-
description: "Edits an existing location zone via the OCS web dashboard. `create_location_zone` is available via API; edit is UI-only. Params: `zone_id`, `new_name`, `add_countries`, `remove_countries`.",
|
|
5986
|
+
description: "Edits an existing location zone via the OCS web dashboard. `create_location_zone` is available via API; edit is UI-only. Params: `zone_id`, `new_name`, `add_countries`, `remove_countries`." + NOTE,
|
|
5390
5987
|
inputSchema: {
|
|
5391
5988
|
zone_id: z4.number().describe("ID of the location zone to edit"),
|
|
5392
5989
|
new_name: z4.string().optional().describe("Rename the location zone"),
|
|
@@ -5395,137 +5992,40 @@ Verify the template no longer appears in the template list.
|
|
|
5395
5992
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5396
5993
|
}
|
|
5397
5994
|
},
|
|
5398
|
-
|
|
5995
|
+
gated(
|
|
5399
5996
|
"ui_edit_location_zone",
|
|
5400
5997
|
"G-19",
|
|
5401
5998
|
"write",
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
` : "") + (args.add_countries && args.add_countries.length > 0 ? `Add countries: ${args.add_countries.join(", ")}
|
|
5407
|
-
` : "") + (args.remove_countries && args.remove_countries.length > 0 ? `Remove countries: ${args.remove_countries.join(", ")}
|
|
5408
|
-
` : "") + `Save the changes and verify the updated country list.
|
|
5409
|
-
`
|
|
5999
|
+
(args, dashboardUrl) => buildEditLocationZonePrompt(
|
|
6000
|
+
args,
|
|
6001
|
+
dashboardUrl
|
|
6002
|
+
)
|
|
5410
6003
|
)
|
|
5411
6004
|
);
|
|
5412
6005
|
server2.registerTool(
|
|
5413
6006
|
"ui_delete_location_zone",
|
|
5414
6007
|
{
|
|
5415
6008
|
title: "Delete Location Zone (UI Agent)",
|
|
5416
|
-
description: "Deletes a location zone via the OCS web dashboard. `create_location_zone` is available via API; delete is UI-only. Params: `zone_id`. WARNING: Zones in use by active templates may not be deletable.",
|
|
6009
|
+
description: "Deletes a location zone via the OCS web dashboard. `create_location_zone` is available via API; delete is UI-only. Params: `zone_id`. WARNING: Zones in use by active templates may not be deletable." + NOTE,
|
|
5417
6010
|
inputSchema: {
|
|
5418
6011
|
zone_id: z4.number().describe("ID of the location zone to delete"),
|
|
5419
6012
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5420
6013
|
}
|
|
5421
6014
|
},
|
|
5422
|
-
|
|
6015
|
+
gated(
|
|
5423
6016
|
"ui_delete_location_zone",
|
|
5424
6017
|
"G-19",
|
|
5425
6018
|
"admin",
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
If the dashboard shows an error (e.g. zone in use by active templates), report the error.
|
|
5431
|
-
Verify the zone no longer appears in the zone list.
|
|
5432
|
-
`
|
|
6019
|
+
(args, dashboardUrl) => buildDeleteLocationZonePrompt(
|
|
6020
|
+
args,
|
|
6021
|
+
dashboardUrl
|
|
6022
|
+
)
|
|
5433
6023
|
)
|
|
5434
6024
|
);
|
|
5435
6025
|
}
|
|
5436
6026
|
|
|
5437
6027
|
// src/tools-ui-agent-ask.ts
|
|
5438
6028
|
import { z as z5 } from "zod";
|
|
5439
|
-
|
|
5440
|
-
// src/manus-webhook.ts
|
|
5441
|
-
var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
5442
|
-
var DEDUP_TTL_SECONDS = 7 * 24 * 3600;
|
|
5443
|
-
var PENDING_ASK_TTL_SECONDS = 24 * 3600;
|
|
5444
|
-
var PENDING_ASK_PREFIX = "manus_pending_ask:";
|
|
5445
|
-
|
|
5446
|
-
// src/manus-client.ts
|
|
5447
|
-
var MANUS_API_BASE2 = "https://api.manus.ai/v2";
|
|
5448
|
-
function getManusKeys(env) {
|
|
5449
|
-
if (!env.MANUS_API_KEY) return null;
|
|
5450
|
-
return {
|
|
5451
|
-
primary: env.MANUS_API_KEY,
|
|
5452
|
-
fallback: env.MANUS_API_KEY_FALLBACK
|
|
5453
|
-
};
|
|
5454
|
-
}
|
|
5455
|
-
var FALLBACK_TRIGGER_CODES = /* @__PURE__ */ new Set([401, 403, 429]);
|
|
5456
|
-
var ManusApiError = class extends Error {
|
|
5457
|
-
constructor(message, httpStatus, manusError, keyUsed, bothFailed) {
|
|
5458
|
-
super(message);
|
|
5459
|
-
this.httpStatus = httpStatus;
|
|
5460
|
-
this.manusError = manusError;
|
|
5461
|
-
this.keyUsed = keyUsed;
|
|
5462
|
-
this.bothFailed = bothFailed;
|
|
5463
|
-
this.name = "ManusApiError";
|
|
5464
|
-
}
|
|
5465
|
-
httpStatus;
|
|
5466
|
-
manusError;
|
|
5467
|
-
keyUsed;
|
|
5468
|
-
bothFailed;
|
|
5469
|
-
};
|
|
5470
|
-
async function withFallback(fn, keys) {
|
|
5471
|
-
const primaryResult = await fn(keys.primary);
|
|
5472
|
-
if (!FALLBACK_TRIGGER_CODES.has(primaryResult._httpStatus)) {
|
|
5473
|
-
return { ...primaryResult, key_used: "primary" };
|
|
5474
|
-
}
|
|
5475
|
-
if (!keys.fallback) {
|
|
5476
|
-
throw new ManusApiError(
|
|
5477
|
-
`Manus API request failed: HTTP ${primaryResult._httpStatus}`,
|
|
5478
|
-
primaryResult._httpStatus,
|
|
5479
|
-
void 0,
|
|
5480
|
-
"primary",
|
|
5481
|
-
false
|
|
5482
|
-
);
|
|
5483
|
-
}
|
|
5484
|
-
const fallbackResult = await fn(keys.fallback);
|
|
5485
|
-
if (!FALLBACK_TRIGGER_CODES.has(fallbackResult._httpStatus)) {
|
|
5486
|
-
return { ...fallbackResult, key_used: "fallback" };
|
|
5487
|
-
}
|
|
5488
|
-
throw new ManusApiError(
|
|
5489
|
-
`Manus API request failed with both keys: HTTP ${fallbackResult._httpStatus}`,
|
|
5490
|
-
fallbackResult._httpStatus,
|
|
5491
|
-
void 0,
|
|
5492
|
-
"fallback",
|
|
5493
|
-
true
|
|
5494
|
-
);
|
|
5495
|
-
}
|
|
5496
|
-
async function manusPost(path, apiKey, body2) {
|
|
5497
|
-
const res = await fetch(`${MANUS_API_BASE2}/${path}`, {
|
|
5498
|
-
method: "POST",
|
|
5499
|
-
headers: {
|
|
5500
|
-
"x-manus-api-key": apiKey,
|
|
5501
|
-
"Content-Type": "application/json"
|
|
5502
|
-
},
|
|
5503
|
-
body: JSON.stringify(body2)
|
|
5504
|
-
});
|
|
5505
|
-
const data = await res.json();
|
|
5506
|
-
return { _httpStatus: res.status, data, key_used: "primary" };
|
|
5507
|
-
}
|
|
5508
|
-
async function sendMessage(keys, taskId, content, opts) {
|
|
5509
|
-
const message = { content };
|
|
5510
|
-
if (opts?.connectors?.length) message.connectors = opts.connectors;
|
|
5511
|
-
if (opts?.enableSkills?.length) message.enable_skills = opts.enableSkills;
|
|
5512
|
-
if (opts?.forceSkills?.length) message.force_skills = opts.forceSkills;
|
|
5513
|
-
const body2 = {
|
|
5514
|
-
task_id: taskId,
|
|
5515
|
-
message
|
|
5516
|
-
};
|
|
5517
|
-
if (opts?.agentProfile) body2.agent_profile = opts.agentProfile;
|
|
5518
|
-
if (opts?.outputSchema) body2.structured_output_schema = opts.outputSchema;
|
|
5519
|
-
return withFallback(
|
|
5520
|
-
(apiKey) => manusPost("task.sendMessage", apiKey, body2),
|
|
5521
|
-
keys
|
|
5522
|
-
);
|
|
5523
|
-
}
|
|
5524
|
-
async function askReply(keys, taskId, reply) {
|
|
5525
|
-
return sendMessage(keys, taskId, reply);
|
|
5526
|
-
}
|
|
5527
|
-
|
|
5528
|
-
// src/tools-ui-agent-ask.ts
|
|
5529
6029
|
var UI_AGENT_ASK_TOOL_SCOPES = {
|
|
5530
6030
|
ui_agent_reply: "write",
|
|
5531
6031
|
ui_agent_list_pending: "read"
|
|
@@ -5535,12 +6035,23 @@ function computeExpiresAt(askedAt) {
|
|
|
5535
6035
|
if (isNaN(asked)) return "";
|
|
5536
6036
|
return new Date(asked + PENDING_ASK_TTL_SECONDS * 1e3).toISOString();
|
|
5537
6037
|
}
|
|
5538
|
-
|
|
5539
|
-
|
|
6038
|
+
var emptyStore = {
|
|
6039
|
+
get: async () => null,
|
|
6040
|
+
put: async () => void 0,
|
|
6041
|
+
delete: async () => void 0
|
|
6042
|
+
};
|
|
6043
|
+
async function callerOwnsTask(ctx, taskId, fallback) {
|
|
6044
|
+
const key = `steel_task:${taskId}`;
|
|
6045
|
+
let raw = null;
|
|
5540
6046
|
try {
|
|
5541
|
-
raw = await ctx.env.CARRIER_USERS.get(
|
|
6047
|
+
raw = await ctx.env.CARRIER_USERS.get(key);
|
|
5542
6048
|
} catch {
|
|
5543
|
-
return false;
|
|
6049
|
+
if (!fallback) return false;
|
|
6050
|
+
try {
|
|
6051
|
+
raw = await fallback.get(key);
|
|
6052
|
+
} catch {
|
|
6053
|
+
return false;
|
|
6054
|
+
}
|
|
5544
6055
|
}
|
|
5545
6056
|
if (!raw) return false;
|
|
5546
6057
|
try {
|
|
@@ -5550,15 +6061,26 @@ async function callerOwnsTask(ctx, taskId) {
|
|
|
5550
6061
|
return false;
|
|
5551
6062
|
}
|
|
5552
6063
|
}
|
|
5553
|
-
function
|
|
6064
|
+
function listableBinding(ctx) {
|
|
6065
|
+
const kv = ctx.env.CARRIER_USERS;
|
|
6066
|
+
return kv && typeof kv.list === "function" ? kv : null;
|
|
6067
|
+
}
|
|
6068
|
+
function registerUiAgentAskTools(server2, ctx, runtime) {
|
|
6069
|
+
const sharedEnv = () => ({
|
|
6070
|
+
...runtime?.env.apiKey ? { apiKey: runtime.env.apiKey } : {},
|
|
6071
|
+
store: ctx.env.CARRIER_USERS ?? runtime?.env.store ?? emptyStore,
|
|
6072
|
+
audit: runtime?.env.audit ?? (() => void 0)
|
|
6073
|
+
});
|
|
5554
6074
|
server2.registerTool(
|
|
5555
6075
|
"ui_agent_reply",
|
|
5556
6076
|
{
|
|
5557
6077
|
title: "Reply to Paused UI Agent Task",
|
|
5558
|
-
description: "Resumes a
|
|
6078
|
+
description: "Resumes a browser automation task that paused to ask a question. Use ui_agent_list_pending to find tasks waiting for input. Provide the task_id and your reply (e.g. a 2FA code, a field value, or a yes/no answer). The reply content is never recorded in audit logs \u2014 only its length is logged.",
|
|
5559
6079
|
inputSchema: {
|
|
5560
|
-
task_id: z5.string().describe("
|
|
5561
|
-
reply: z5.string().describe(
|
|
6080
|
+
task_id: z5.string().describe("Task ID to resume (from ui_agent_list_pending)"),
|
|
6081
|
+
reply: z5.string().describe(
|
|
6082
|
+
"Your answer to the agent's question (e.g. a 2FA code or confirmation)"
|
|
6083
|
+
)
|
|
5562
6084
|
}
|
|
5563
6085
|
},
|
|
5564
6086
|
async (args) => {
|
|
@@ -5582,24 +6104,14 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5582
6104
|
]
|
|
5583
6105
|
};
|
|
5584
6106
|
}
|
|
5585
|
-
const
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
type: "text",
|
|
5592
|
-
text: JSON.stringify({
|
|
5593
|
-
error: "manus_api_not_configured",
|
|
5594
|
-
message: "MANUS_API_KEY is not configured on this deployment."
|
|
5595
|
-
})
|
|
5596
|
-
}
|
|
5597
|
-
]
|
|
5598
|
-
};
|
|
6107
|
+
const env = sharedEnv();
|
|
6108
|
+
let pendingRaw = null;
|
|
6109
|
+
try {
|
|
6110
|
+
pendingRaw = await env.store.get(`${PENDING_ASK_PREFIX}${task_id}`);
|
|
6111
|
+
} catch {
|
|
6112
|
+
pendingRaw = null;
|
|
5599
6113
|
}
|
|
5600
|
-
|
|
5601
|
-
const pendingRaw = await ctx.env.CARRIER_USERS.get(pendingKey);
|
|
5602
|
-
if (pendingRaw === null || !await callerOwnsTask(ctx, task_id)) {
|
|
6114
|
+
if (pendingRaw === null || !await callerOwnsTask(ctx, task_id, runtime?.env.store)) {
|
|
5603
6115
|
return {
|
|
5604
6116
|
isError: true,
|
|
5605
6117
|
content: [
|
|
@@ -5613,75 +6125,42 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5613
6125
|
]
|
|
5614
6126
|
};
|
|
5615
6127
|
}
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
duration_ms: Date.now() - start,
|
|
5627
|
-
event_type: "ui_agent_resume",
|
|
5628
|
-
manus_task_id: task_id
|
|
5629
|
-
});
|
|
6128
|
+
const outcome = await resumeUiAgentTask(task_id, reply, env);
|
|
6129
|
+
ctx.audit({
|
|
6130
|
+
tool_name: "ui_agent_reply",
|
|
6131
|
+
ocs_method: "[ui-agent:ask-reply]",
|
|
6132
|
+
status: outcome.ok ? "ui_agent_resumed" : "error",
|
|
6133
|
+
dry_run: false,
|
|
6134
|
+
duration_ms: Date.now() - start,
|
|
6135
|
+
event_type: "ui_agent_resume"
|
|
6136
|
+
});
|
|
6137
|
+
if (!outcome.ok) {
|
|
5630
6138
|
return {
|
|
5631
6139
|
isError: true,
|
|
5632
6140
|
content: [
|
|
5633
6141
|
{
|
|
5634
6142
|
type: "text",
|
|
5635
|
-
text:
|
|
6143
|
+
text: JSON.stringify({
|
|
6144
|
+
error: "resume_failed",
|
|
6145
|
+
message: outcome.error ?? "The task could not be resumed."
|
|
6146
|
+
})
|
|
5636
6147
|
}
|
|
5637
6148
|
]
|
|
5638
6149
|
};
|
|
5639
6150
|
}
|
|
5640
|
-
if (!result2.ok) {
|
|
5641
|
-
ctx.audit({
|
|
5642
|
-
tool_name: "ui_agent_reply",
|
|
5643
|
-
ocs_method: "[ui-agent:ask-reply]",
|
|
5644
|
-
status: "error",
|
|
5645
|
-
dry_run: false,
|
|
5646
|
-
duration_ms: Date.now() - start,
|
|
5647
|
-
event_type: "ui_agent_resume",
|
|
5648
|
-
manus_task_id: task_id
|
|
5649
|
-
});
|
|
5650
|
-
return {
|
|
5651
|
-
isError: true,
|
|
5652
|
-
content: [
|
|
5653
|
-
{
|
|
5654
|
-
type: "text",
|
|
5655
|
-
text: JSON.stringify({
|
|
5656
|
-
error: "manus_reply_failed",
|
|
5657
|
-
message: result2.error?.message ?? "Manus task.reply returned ok=false",
|
|
5658
|
-
code: result2.error?.code,
|
|
5659
|
-
task_id
|
|
5660
|
-
})
|
|
5661
|
-
}
|
|
5662
|
-
]
|
|
5663
|
-
};
|
|
5664
|
-
}
|
|
5665
|
-
ctx.audit({
|
|
5666
|
-
tool_name: "ui_agent_reply",
|
|
5667
|
-
ocs_method: "[ui-agent:ask-reply]",
|
|
5668
|
-
status: "ui_agent_resumed",
|
|
5669
|
-
dry_run: false,
|
|
5670
|
-
duration_ms: Date.now() - start,
|
|
5671
|
-
event_type: "ui_agent_resume",
|
|
5672
|
-
manus_task_id: task_id
|
|
5673
|
-
});
|
|
5674
|
-
await ctx.env.CARRIER_USERS.delete(pendingKey);
|
|
5675
6151
|
return {
|
|
5676
6152
|
content: [
|
|
5677
6153
|
{
|
|
5678
6154
|
type: "text",
|
|
5679
|
-
text: JSON.stringify(
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
6155
|
+
text: JSON.stringify(
|
|
6156
|
+
{
|
|
6157
|
+
status: "ui_agent_resumed",
|
|
6158
|
+
task_id,
|
|
6159
|
+
reply_length: reply.length
|
|
6160
|
+
},
|
|
6161
|
+
null,
|
|
6162
|
+
2
|
|
6163
|
+
)
|
|
5685
6164
|
}
|
|
5686
6165
|
]
|
|
5687
6166
|
};
|
|
@@ -5691,7 +6170,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5691
6170
|
"ui_agent_list_pending",
|
|
5692
6171
|
{
|
|
5693
6172
|
title: "List Pending UI Agent Tasks (Waiting for Input)",
|
|
5694
|
-
description: "Returns
|
|
6173
|
+
description: "Returns browser automation tasks currently paused waiting for human input. Shows the agent's question, task URL, and when it was asked. Entries expire after 24 hours. Use ui_agent_reply to resume a task.",
|
|
5695
6174
|
inputSchema: {}
|
|
5696
6175
|
},
|
|
5697
6176
|
async () => {
|
|
@@ -5706,61 +6185,61 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5706
6185
|
]
|
|
5707
6186
|
};
|
|
5708
6187
|
}
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
pending_tasks: [],
|
|
5731
|
-
count: 0,
|
|
5732
|
-
message: "No Manus tasks are currently waiting for input."
|
|
5733
|
-
}, null, 2)
|
|
5734
|
-
}
|
|
5735
|
-
]
|
|
5736
|
-
};
|
|
6188
|
+
const kv = listableBinding(ctx);
|
|
6189
|
+
let taskIds;
|
|
6190
|
+
if (kv) {
|
|
6191
|
+
try {
|
|
6192
|
+
const listing = await kv.list({ prefix: PENDING_ASK_PREFIX });
|
|
6193
|
+
taskIds = listing.keys.map(
|
|
6194
|
+
(k) => k.name.slice(PENDING_ASK_PREFIX.length)
|
|
6195
|
+
);
|
|
6196
|
+
} catch (err7) {
|
|
6197
|
+
return {
|
|
6198
|
+
isError: true,
|
|
6199
|
+
content: [
|
|
6200
|
+
{
|
|
6201
|
+
type: "text",
|
|
6202
|
+
text: `Error listing pending tasks: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
6203
|
+
}
|
|
6204
|
+
]
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
} else {
|
|
6208
|
+
taskIds = [...runtime?.dispatchedTaskIds ?? []];
|
|
5737
6209
|
}
|
|
6210
|
+
const store = sharedEnv().store;
|
|
5738
6211
|
const entries = await Promise.all(
|
|
5739
|
-
|
|
5740
|
-
|
|
6212
|
+
taskIds.map(async (taskId) => {
|
|
6213
|
+
let raw;
|
|
6214
|
+
try {
|
|
6215
|
+
raw = await store.get(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
6216
|
+
} catch {
|
|
6217
|
+
return null;
|
|
6218
|
+
}
|
|
5741
6219
|
if (!raw) return null;
|
|
6220
|
+
if (!await callerOwnsTask(ctx, taskId, runtime?.env.store)) {
|
|
6221
|
+
return null;
|
|
6222
|
+
}
|
|
5742
6223
|
try {
|
|
5743
6224
|
const parsed = JSON.parse(raw);
|
|
5744
|
-
|
|
5745
|
-
if (!await callerOwnsTask(ctx, taskId)) return null;
|
|
5746
|
-
return {
|
|
5747
|
-
...parsed,
|
|
5748
|
-
expires_at: computeExpiresAt(parsed.asked_at)
|
|
5749
|
-
};
|
|
6225
|
+
return { ...parsed, expires_at: computeExpiresAt(parsed.asked_at) };
|
|
5750
6226
|
} catch {
|
|
5751
6227
|
return null;
|
|
5752
6228
|
}
|
|
5753
6229
|
})
|
|
5754
6230
|
);
|
|
5755
|
-
const validEntries = entries.filter(
|
|
6231
|
+
const validEntries = entries.filter(
|
|
6232
|
+
(e) => e !== null
|
|
6233
|
+
);
|
|
5756
6234
|
return {
|
|
5757
6235
|
content: [
|
|
5758
6236
|
{
|
|
5759
6237
|
type: "text",
|
|
5760
|
-
text: JSON.stringify(
|
|
5761
|
-
pending_tasks: validEntries,
|
|
5762
|
-
|
|
5763
|
-
|
|
6238
|
+
text: JSON.stringify(
|
|
6239
|
+
{ pending_tasks: validEntries, count: validEntries.length },
|
|
6240
|
+
null,
|
|
6241
|
+
2
|
|
6242
|
+
)
|
|
5764
6243
|
}
|
|
5765
6244
|
]
|
|
5766
6245
|
};
|
|
@@ -5770,277 +6249,6 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5770
6249
|
|
|
5771
6250
|
// src/tools-ui-agent-schedule.ts
|
|
5772
6251
|
import { z as z6 } from "zod";
|
|
5773
|
-
|
|
5774
|
-
// src/manus-schedule.ts
|
|
5775
|
-
var ManusScheduleError = class extends Error {
|
|
5776
|
-
constructor(message, statusCode) {
|
|
5777
|
-
super(message);
|
|
5778
|
-
this.statusCode = statusCode;
|
|
5779
|
-
this.name = "ManusScheduleError";
|
|
5780
|
-
}
|
|
5781
|
-
statusCode;
|
|
5782
|
-
};
|
|
5783
|
-
function expandCronMinuteField(minuteField) {
|
|
5784
|
-
if (minuteField === "*" || minuteField.includes(" ")) {
|
|
5785
|
-
return null;
|
|
5786
|
-
}
|
|
5787
|
-
const tokens = minuteField.split(",").map((t) => t.trim()).filter(Boolean);
|
|
5788
|
-
const set = /* @__PURE__ */ new Set();
|
|
5789
|
-
for (const token2 of tokens) {
|
|
5790
|
-
const stepWildcard = /^[*]\/(\d+)$/.exec(token2);
|
|
5791
|
-
if (stepWildcard) {
|
|
5792
|
-
const step = parseInt(stepWildcard[1] ?? "0", 10);
|
|
5793
|
-
if (step < 1) return null;
|
|
5794
|
-
for (let m = 0; m < 60; m += step) set.add(m);
|
|
5795
|
-
continue;
|
|
5796
|
-
}
|
|
5797
|
-
const rangeWithStep = /^(\d+)-(\d+)\/(\d+)$/.exec(token2);
|
|
5798
|
-
if (rangeWithStep) {
|
|
5799
|
-
const start = parseInt(rangeWithStep[1] ?? "0", 10);
|
|
5800
|
-
const end = parseInt(rangeWithStep[2] ?? "0", 10);
|
|
5801
|
-
const step = parseInt(rangeWithStep[3] ?? "0", 10);
|
|
5802
|
-
if (step < 1 || start > end) return null;
|
|
5803
|
-
for (let m = start; m <= end; m += step) set.add(m);
|
|
5804
|
-
continue;
|
|
5805
|
-
}
|
|
5806
|
-
const rangeOnly = /^(\d+)-(\d+)$/.exec(token2);
|
|
5807
|
-
if (rangeOnly) {
|
|
5808
|
-
const start = parseInt(rangeOnly[1] ?? "0", 10);
|
|
5809
|
-
const end = parseInt(rangeOnly[2] ?? "0", 10);
|
|
5810
|
-
if (start > end) return null;
|
|
5811
|
-
for (let m = start; m <= end; m++) set.add(m);
|
|
5812
|
-
continue;
|
|
5813
|
-
}
|
|
5814
|
-
const single = /^(\d+)$/.exec(token2);
|
|
5815
|
-
if (single) {
|
|
5816
|
-
set.add(parseInt(single[1] ?? "0", 10));
|
|
5817
|
-
continue;
|
|
5818
|
-
}
|
|
5819
|
-
return null;
|
|
5820
|
-
}
|
|
5821
|
-
const arr = Array.from(set).sort((a, b) => a - b);
|
|
5822
|
-
for (const v of arr) {
|
|
5823
|
-
if (v < 0 || v > 59) return null;
|
|
5824
|
-
}
|
|
5825
|
-
return arr;
|
|
5826
|
-
}
|
|
5827
|
-
function minuteFieldViolatesFiveMinuteRule(minuteField, fullCron) {
|
|
5828
|
-
const minutes = expandCronMinuteField(minuteField);
|
|
5829
|
-
if (minutes === null) {
|
|
5830
|
-
return `Cron expression rejected: unrecognized or unsupported minute field '${minuteField}'. Minimum interval is 5 minutes; use lists, ranges with step \u2265 5, or */5 or higher.`;
|
|
5831
|
-
}
|
|
5832
|
-
if (minutes.length === 0) {
|
|
5833
|
-
return `Cron expression rejected: minute field '${minuteField}' expands to no valid minutes.`;
|
|
5834
|
-
}
|
|
5835
|
-
if (minutes.length === 1) {
|
|
5836
|
-
return null;
|
|
5837
|
-
}
|
|
5838
|
-
for (let i = 1; i < minutes.length; i++) {
|
|
5839
|
-
const gap = (minutes[i] ?? 0) - (minutes[i - 1] ?? 0);
|
|
5840
|
-
if (gap < 5) {
|
|
5841
|
-
return `Cron expression rejected: '${fullCron}' implies a ${gap}-minute gap in the minute field. Minimum allowed interval is 5 minutes.`;
|
|
5842
|
-
}
|
|
5843
|
-
}
|
|
5844
|
-
const wrapGap = 60 - (minutes[minutes.length - 1] ?? 0) + (minutes[0] ?? 0);
|
|
5845
|
-
if (wrapGap < 5) {
|
|
5846
|
-
return `Cron expression rejected: '${fullCron}' implies a ${wrapGap}-minute wraparound gap in the minute field. Minimum allowed interval is 5 minutes.`;
|
|
5847
|
-
}
|
|
5848
|
-
return null;
|
|
5849
|
-
}
|
|
5850
|
-
function validateCron(cron) {
|
|
5851
|
-
const parts = cron.trim().split(/\s+/);
|
|
5852
|
-
if (parts.length !== 5) {
|
|
5853
|
-
return `Invalid cron expression: expected 5 fields (minute hour day month weekday), got ${parts.length}.`;
|
|
5854
|
-
}
|
|
5855
|
-
const [minuteField] = parts;
|
|
5856
|
-
if (minuteField === "*") {
|
|
5857
|
-
return "Cron expression rejected: '* * * * *' runs every minute. Minimum allowed interval is 5 minutes.";
|
|
5858
|
-
}
|
|
5859
|
-
return minuteFieldViolatesFiveMinuteRule(minuteField ?? "", cron);
|
|
5860
|
-
}
|
|
5861
|
-
async function createSchedule(apiKey, params) {
|
|
5862
|
-
const body2 = {
|
|
5863
|
-
name: params.name,
|
|
5864
|
-
cron: params.cron,
|
|
5865
|
-
prompt_template: params.prompt_template,
|
|
5866
|
-
agent_profile: params.agent_profile ?? "manus-1.6-lite",
|
|
5867
|
-
interactive_mode: false,
|
|
5868
|
-
hide_in_task_list: false
|
|
5869
|
-
};
|
|
5870
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.create`, {
|
|
5871
|
-
method: "POST",
|
|
5872
|
-
headers: {
|
|
5873
|
-
"x-manus-api-key": apiKey,
|
|
5874
|
-
"Content-Type": "application/json"
|
|
5875
|
-
},
|
|
5876
|
-
body: JSON.stringify(body2)
|
|
5877
|
-
});
|
|
5878
|
-
if (!res.ok) {
|
|
5879
|
-
throw new ManusScheduleError(
|
|
5880
|
-
`schedule.create failed: HTTP ${res.status}`,
|
|
5881
|
-
res.status
|
|
5882
|
-
);
|
|
5883
|
-
}
|
|
5884
|
-
return await res.json();
|
|
5885
|
-
}
|
|
5886
|
-
async function listSchedules(apiKey) {
|
|
5887
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.list`, {
|
|
5888
|
-
headers: { "x-manus-api-key": apiKey }
|
|
5889
|
-
});
|
|
5890
|
-
if (!res.ok) {
|
|
5891
|
-
throw new ManusScheduleError(
|
|
5892
|
-
`schedule.list failed: HTTP ${res.status}`,
|
|
5893
|
-
res.status
|
|
5894
|
-
);
|
|
5895
|
-
}
|
|
5896
|
-
const data = await res.json();
|
|
5897
|
-
if (Array.isArray(data)) {
|
|
5898
|
-
return { ok: true, schedules: data };
|
|
5899
|
-
}
|
|
5900
|
-
return data;
|
|
5901
|
-
}
|
|
5902
|
-
async function deleteSchedule(apiKey, scheduleId) {
|
|
5903
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.delete`, {
|
|
5904
|
-
method: "POST",
|
|
5905
|
-
headers: {
|
|
5906
|
-
"x-manus-api-key": apiKey,
|
|
5907
|
-
"Content-Type": "application/json"
|
|
5908
|
-
},
|
|
5909
|
-
body: JSON.stringify({ schedule_id: scheduleId })
|
|
5910
|
-
});
|
|
5911
|
-
if (!res.ok) {
|
|
5912
|
-
throw new ManusScheduleError(
|
|
5913
|
-
`schedule.delete failed: HTTP ${res.status}`,
|
|
5914
|
-
res.status
|
|
5915
|
-
);
|
|
5916
|
-
}
|
|
5917
|
-
return await res.json();
|
|
5918
|
-
}
|
|
5919
|
-
async function pauseSchedule(apiKey, scheduleId) {
|
|
5920
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.pause`, {
|
|
5921
|
-
method: "POST",
|
|
5922
|
-
headers: {
|
|
5923
|
-
"x-manus-api-key": apiKey,
|
|
5924
|
-
"Content-Type": "application/json"
|
|
5925
|
-
},
|
|
5926
|
-
body: JSON.stringify({ schedule_id: scheduleId })
|
|
5927
|
-
});
|
|
5928
|
-
if (!res.ok) {
|
|
5929
|
-
throw new ManusScheduleError(
|
|
5930
|
-
`schedule.pause failed: HTTP ${res.status}`,
|
|
5931
|
-
res.status
|
|
5932
|
-
);
|
|
5933
|
-
}
|
|
5934
|
-
return await res.json();
|
|
5935
|
-
}
|
|
5936
|
-
async function resumeSchedule(apiKey, scheduleId) {
|
|
5937
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.resume`, {
|
|
5938
|
-
method: "POST",
|
|
5939
|
-
headers: {
|
|
5940
|
-
"x-manus-api-key": apiKey,
|
|
5941
|
-
"Content-Type": "application/json"
|
|
5942
|
-
},
|
|
5943
|
-
body: JSON.stringify({ schedule_id: scheduleId })
|
|
5944
|
-
});
|
|
5945
|
-
if (!res.ok) {
|
|
5946
|
-
throw new ManusScheduleError(
|
|
5947
|
-
`schedule.resume failed: HTTP ${res.status}`,
|
|
5948
|
-
res.status
|
|
5949
|
-
);
|
|
5950
|
-
}
|
|
5951
|
-
return await res.json();
|
|
5952
|
-
}
|
|
5953
|
-
|
|
5954
|
-
// src/manus-usage.ts
|
|
5955
|
-
var ManusUsageError = class extends Error {
|
|
5956
|
-
constructor(message, statusCode) {
|
|
5957
|
-
super(message);
|
|
5958
|
-
this.statusCode = statusCode;
|
|
5959
|
-
this.name = "ManusUsageError";
|
|
5960
|
-
}
|
|
5961
|
-
statusCode;
|
|
5962
|
-
};
|
|
5963
|
-
var CACHE_KEY = "manus_usage_cache";
|
|
5964
|
-
var CACHE_TTL_SECONDS = 60;
|
|
5965
|
-
var USAGE_THRESHOLD_WARNING = 1e3;
|
|
5966
|
-
var USAGE_THRESHOLD_CRITICAL = 100;
|
|
5967
|
-
async function fetchUsageFromApi(apiKey) {
|
|
5968
|
-
const now = /* @__PURE__ */ new Date();
|
|
5969
|
-
const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
5970
|
-
const [usageRes, creditsRes] = await Promise.all([
|
|
5971
|
-
fetch(`${MANUS_API_BASE}/usage.get`, {
|
|
5972
|
-
headers: { "x-manus-api-key": apiKey }
|
|
5973
|
-
}),
|
|
5974
|
-
fetch(`${MANUS_API_BASE}/credits.get`, {
|
|
5975
|
-
headers: { "x-manus-api-key": apiKey }
|
|
5976
|
-
})
|
|
5977
|
-
]);
|
|
5978
|
-
if (!usageRes.ok && !creditsRes.ok) {
|
|
5979
|
-
throw new ManusUsageError(
|
|
5980
|
-
`Manus usage APIs failed: usage.get HTTP ${usageRes.status}, credits.get HTTP ${creditsRes.status}`,
|
|
5981
|
-
Math.max(usageRes.status, creditsRes.status)
|
|
5982
|
-
);
|
|
5983
|
-
}
|
|
5984
|
-
let spentCredits = 0;
|
|
5985
|
-
let remainingCredits = 0;
|
|
5986
|
-
let taskCount = 0;
|
|
5987
|
-
if (usageRes.ok) {
|
|
5988
|
-
const usageData = await usageRes.json();
|
|
5989
|
-
spentCredits = usageData.spent_credits ?? usageData.credits_used ?? 0;
|
|
5990
|
-
taskCount = usageData.task_count ?? usageData.tasks_run ?? 0;
|
|
5991
|
-
if (usageData.remaining_credits !== void 0 || usageData.credits_remaining !== void 0) {
|
|
5992
|
-
remainingCredits = usageData.remaining_credits ?? usageData.credits_remaining ?? 0;
|
|
5993
|
-
}
|
|
5994
|
-
}
|
|
5995
|
-
if (creditsRes.ok) {
|
|
5996
|
-
const creditsData = await creditsRes.json();
|
|
5997
|
-
const fromCreditsApi = creditsData.remaining_credits ?? creditsData.credits_remaining ?? creditsData.balance;
|
|
5998
|
-
if (fromCreditsApi !== void 0) {
|
|
5999
|
-
remainingCredits = fromCreditsApi;
|
|
6000
|
-
}
|
|
6001
|
-
}
|
|
6002
|
-
return { month, spent_credits: spentCredits, remaining_credits: remainingCredits, task_count: taskCount };
|
|
6003
|
-
}
|
|
6004
|
-
async function getUsage(apiKey, env) {
|
|
6005
|
-
const cached = await env.CARRIER_USERS.get(CACHE_KEY, "json");
|
|
6006
|
-
const nowMs = Date.now();
|
|
6007
|
-
if (cached && nowMs - cached.fetched_at < CACHE_TTL_SECONDS * 1e3) {
|
|
6008
|
-
return { data: cached.data, from_cache: true };
|
|
6009
|
-
}
|
|
6010
|
-
const data = await fetchUsageFromApi(apiKey);
|
|
6011
|
-
await env.CARRIER_USERS.put(
|
|
6012
|
-
CACHE_KEY,
|
|
6013
|
-
JSON.stringify({ data, fetched_at: nowMs }),
|
|
6014
|
-
{ expirationTtl: CACHE_TTL_SECONDS }
|
|
6015
|
-
);
|
|
6016
|
-
return { data, from_cache: false };
|
|
6017
|
-
}
|
|
6018
|
-
function emitUsageThresholdIfNeeded(env, data) {
|
|
6019
|
-
const { remaining_credits, month, task_count } = data;
|
|
6020
|
-
if (remaining_credits < USAGE_THRESHOLD_CRITICAL) {
|
|
6021
|
-
writeUsageThresholdAudit(env, "critical", remaining_credits, month, task_count);
|
|
6022
|
-
} else if (remaining_credits < USAGE_THRESHOLD_WARNING) {
|
|
6023
|
-
writeUsageThresholdAudit(env, "warning", remaining_credits, month, task_count);
|
|
6024
|
-
}
|
|
6025
|
-
}
|
|
6026
|
-
function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCount) {
|
|
6027
|
-
try {
|
|
6028
|
-
env.AUDIT_LOG.writeDataPoint({
|
|
6029
|
-
blobs: [
|
|
6030
|
-
"manus_usage_threshold",
|
|
6031
|
-
severity,
|
|
6032
|
-
month,
|
|
6033
|
-
String(taskCount)
|
|
6034
|
-
],
|
|
6035
|
-
doubles: [remainingCredits],
|
|
6036
|
-
indexes: ["manus_usage"]
|
|
6037
|
-
});
|
|
6038
|
-
} catch (err7) {
|
|
6039
|
-
console.error(`[manus-usage] threshold audit write failed: ${err7.message}`);
|
|
6040
|
-
}
|
|
6041
|
-
}
|
|
6042
|
-
|
|
6043
|
-
// src/tools-ui-agent-schedule.ts
|
|
6044
6252
|
var UI_AGENT_SCHEDULE_TOOL_SCOPES = {
|
|
6045
6253
|
ui_agent_schedule_create: "admin",
|
|
6046
6254
|
ui_agent_schedule_list: "read",
|
|
@@ -6049,20 +6257,7 @@ var UI_AGENT_SCHEDULE_TOOL_SCOPES = {
|
|
|
6049
6257
|
ui_agent_schedule_resume: "admin",
|
|
6050
6258
|
ui_agent_usage: "read"
|
|
6051
6259
|
};
|
|
6052
|
-
|
|
6053
|
-
return {
|
|
6054
|
-
isError: true,
|
|
6055
|
-
content: [
|
|
6056
|
-
{
|
|
6057
|
-
type: "text",
|
|
6058
|
-
text: JSON.stringify({
|
|
6059
|
-
error: "manus_api_not_configured",
|
|
6060
|
-
message: "MANUS_API_KEY is not configured on this Carrier MCP deployment. Schedule and usage tools require a Manus API key. Contact your Carrier MCP administrator."
|
|
6061
|
-
})
|
|
6062
|
-
}
|
|
6063
|
-
]
|
|
6064
|
-
};
|
|
6065
|
-
}
|
|
6260
|
+
var STDIO_NOTE = " Requires the remote Carrier MCP Worker deployment \u2014 a local CLI has no recurring runtime, so schedules cannot fire here.";
|
|
6066
6261
|
function scopeError(toolName, required, actual) {
|
|
6067
6262
|
return {
|
|
6068
6263
|
isError: true,
|
|
@@ -6074,427 +6269,126 @@ function scopeError(toolName, required, actual) {
|
|
|
6074
6269
|
]
|
|
6075
6270
|
};
|
|
6076
6271
|
}
|
|
6077
|
-
function
|
|
6272
|
+
function scopeDenied(ctx, toolName, required) {
|
|
6273
|
+
ctx.audit({
|
|
6274
|
+
tool_name: toolName,
|
|
6275
|
+
ocs_method: `[ui-agent:${toolName}]`,
|
|
6276
|
+
status: "scope_denied",
|
|
6277
|
+
dry_run: false,
|
|
6278
|
+
duration_ms: 0
|
|
6279
|
+
});
|
|
6280
|
+
return scopeError(toolName, required, ctx.props.scope);
|
|
6281
|
+
}
|
|
6282
|
+
function registerScheduleAndUsageTools(server2, ctx, runtime) {
|
|
6078
6283
|
server2.registerTool(
|
|
6079
6284
|
"ui_agent_schedule_create",
|
|
6080
6285
|
{
|
|
6081
|
-
title: "Create
|
|
6082
|
-
description: "Creates a recurring
|
|
6286
|
+
title: "Create UI Agent Schedule",
|
|
6287
|
+
description: "Creates a recurring browser-agent run on a cron schedule, to automate periodic OCS audits, fleet health checks, or any recurring browser-automation task. Minimum interval: 5 minutes. Requires admin scope." + STDIO_NOTE,
|
|
6083
6288
|
inputSchema: {
|
|
6084
6289
|
name: z6.string().describe("Human-readable name for this schedule"),
|
|
6085
6290
|
cron: z6.string().describe(
|
|
6086
6291
|
"Standard 5-field cron expression (minute hour day month weekday). Minimum interval: 5 minutes. Example: '0 */6 * * *' = every 6 hours."
|
|
6087
6292
|
),
|
|
6088
|
-
prompt_template: z6.string().describe(
|
|
6089
|
-
|
|
6293
|
+
prompt_template: z6.string().describe(
|
|
6294
|
+
"Agent prompt/task template the browser agent will execute on each run"
|
|
6295
|
+
),
|
|
6296
|
+
profile: z6.string().optional().describe(
|
|
6297
|
+
"Browser-agent profile to use. Defaults to the deployment's configured profile."
|
|
6298
|
+
)
|
|
6090
6299
|
}
|
|
6091
6300
|
},
|
|
6092
|
-
async (
|
|
6093
|
-
const start = Date.now();
|
|
6094
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6095
|
-
ctx.audit({
|
|
6096
|
-
tool_name: "ui_agent_schedule_create",
|
|
6097
|
-
ocs_method: "[manus:schedule.create]",
|
|
6098
|
-
status: "scope_denied",
|
|
6099
|
-
dry_run: false,
|
|
6100
|
-
duration_ms: 0
|
|
6101
|
-
});
|
|
6102
|
-
return scopeError("ui_agent_schedule_create", "admin", ctx.props.scope);
|
|
6103
|
-
}
|
|
6104
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6105
|
-
return noManusKeyError2();
|
|
6106
|
-
}
|
|
6107
|
-
const cronError = validateCron(args.cron);
|
|
6108
|
-
if (cronError) {
|
|
6109
|
-
return {
|
|
6110
|
-
isError: true,
|
|
6111
|
-
content: [{ type: "text", text: JSON.stringify({ error: "invalid_cron", message: cronError }) }]
|
|
6112
|
-
};
|
|
6113
|
-
}
|
|
6114
|
-
try {
|
|
6115
|
-
const result2 = await createSchedule(ctx.env.MANUS_API_KEY, {
|
|
6116
|
-
name: args.name,
|
|
6117
|
-
cron: args.cron,
|
|
6118
|
-
prompt_template: args.prompt_template,
|
|
6119
|
-
agent_profile: args.profile
|
|
6120
|
-
});
|
|
6121
|
-
ctx.audit({
|
|
6122
|
-
tool_name: "ui_agent_schedule_create",
|
|
6123
|
-
ocs_method: "[manus:schedule.create]",
|
|
6124
|
-
status: result2.ok ? "ok" : "error",
|
|
6125
|
-
dry_run: false,
|
|
6126
|
-
duration_ms: Date.now() - start,
|
|
6127
|
-
event_type: "ui_agent_dispatch"
|
|
6128
|
-
});
|
|
6129
|
-
if (!result2.ok || !result2.schedule_id) {
|
|
6130
|
-
return {
|
|
6131
|
-
isError: true,
|
|
6132
|
-
content: [
|
|
6133
|
-
{
|
|
6134
|
-
type: "text",
|
|
6135
|
-
text: JSON.stringify({
|
|
6136
|
-
error: "schedule_create_failed",
|
|
6137
|
-
message: result2.error?.message ?? "Manus schedule.create returned ok=false",
|
|
6138
|
-
code: result2.error?.code
|
|
6139
|
-
})
|
|
6140
|
-
}
|
|
6141
|
-
]
|
|
6142
|
-
};
|
|
6143
|
-
}
|
|
6144
|
-
return {
|
|
6145
|
-
content: [
|
|
6146
|
-
{
|
|
6147
|
-
type: "text",
|
|
6148
|
-
text: JSON.stringify({
|
|
6149
|
-
status: "created",
|
|
6150
|
-
schedule_id: result2.schedule_id,
|
|
6151
|
-
name: args.name,
|
|
6152
|
-
cron: args.cron
|
|
6153
|
-
}, null, 2)
|
|
6154
|
-
}
|
|
6155
|
-
]
|
|
6156
|
-
};
|
|
6157
|
-
} catch (err7) {
|
|
6158
|
-
ctx.audit({
|
|
6159
|
-
tool_name: "ui_agent_schedule_create",
|
|
6160
|
-
ocs_method: "[manus:schedule.create]",
|
|
6161
|
-
status: "error",
|
|
6162
|
-
dry_run: false,
|
|
6163
|
-
duration_ms: Date.now() - start,
|
|
6164
|
-
event_type: "ui_agent_dispatch"
|
|
6165
|
-
});
|
|
6166
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6167
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6168
|
-
}
|
|
6169
|
-
}
|
|
6301
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_create", "admin") : uiAgentStubError("ui_agent_schedule_create", "recurring_runtime")
|
|
6170
6302
|
);
|
|
6171
6303
|
server2.registerTool(
|
|
6172
6304
|
"ui_agent_schedule_list",
|
|
6173
6305
|
{
|
|
6174
|
-
title: "List
|
|
6175
|
-
description: "Lists all
|
|
6306
|
+
title: "List UI Agent Schedules",
|
|
6307
|
+
description: "Lists all recurring browser-agent schedules configured for this deployment. Returns schedule IDs, names, cron expressions, status (active/paused), and next/last run timestamps." + STDIO_NOTE,
|
|
6176
6308
|
inputSchema: {}
|
|
6177
6309
|
},
|
|
6178
|
-
async () =>
|
|
6179
|
-
const start = Date.now();
|
|
6180
|
-
if (!ctx.props.scope.includes("read")) {
|
|
6181
|
-
ctx.audit({
|
|
6182
|
-
tool_name: "ui_agent_schedule_list",
|
|
6183
|
-
ocs_method: "[manus:schedule.list]",
|
|
6184
|
-
status: "scope_denied",
|
|
6185
|
-
dry_run: false,
|
|
6186
|
-
duration_ms: 0
|
|
6187
|
-
});
|
|
6188
|
-
return scopeError("ui_agent_schedule_list", "read", ctx.props.scope);
|
|
6189
|
-
}
|
|
6190
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6191
|
-
return noManusKeyError2();
|
|
6192
|
-
}
|
|
6193
|
-
try {
|
|
6194
|
-
const result2 = await listSchedules(ctx.env.MANUS_API_KEY);
|
|
6195
|
-
ctx.audit({
|
|
6196
|
-
tool_name: "ui_agent_schedule_list",
|
|
6197
|
-
ocs_method: "[manus:schedule.list]",
|
|
6198
|
-
status: result2.ok ? "ok" : "error",
|
|
6199
|
-
dry_run: false,
|
|
6200
|
-
duration_ms: Date.now() - start
|
|
6201
|
-
});
|
|
6202
|
-
if (!result2.ok) {
|
|
6203
|
-
return {
|
|
6204
|
-
isError: true,
|
|
6205
|
-
content: [
|
|
6206
|
-
{
|
|
6207
|
-
type: "text",
|
|
6208
|
-
text: JSON.stringify({
|
|
6209
|
-
error: "schedule_list_failed",
|
|
6210
|
-
message: result2.error?.message ?? "Manus schedule.list returned ok=false"
|
|
6211
|
-
})
|
|
6212
|
-
}
|
|
6213
|
-
]
|
|
6214
|
-
};
|
|
6215
|
-
}
|
|
6216
|
-
return {
|
|
6217
|
-
content: [
|
|
6218
|
-
{
|
|
6219
|
-
type: "text",
|
|
6220
|
-
text: JSON.stringify({ schedules: result2.schedules ?? [] }, null, 2)
|
|
6221
|
-
}
|
|
6222
|
-
]
|
|
6223
|
-
};
|
|
6224
|
-
} catch (err7) {
|
|
6225
|
-
ctx.audit({
|
|
6226
|
-
tool_name: "ui_agent_schedule_list",
|
|
6227
|
-
ocs_method: "[manus:schedule.list]",
|
|
6228
|
-
status: "error",
|
|
6229
|
-
dry_run: false,
|
|
6230
|
-
duration_ms: Date.now() - start
|
|
6231
|
-
});
|
|
6232
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6233
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6234
|
-
}
|
|
6235
|
-
}
|
|
6310
|
+
async () => !ctx.props.scope.includes("read") ? scopeDenied(ctx, "ui_agent_schedule_list", "read") : uiAgentStubError("ui_agent_schedule_list", "recurring_runtime")
|
|
6236
6311
|
);
|
|
6237
|
-
server2.registerTool(
|
|
6238
|
-
"ui_agent_schedule_delete",
|
|
6239
|
-
{
|
|
6240
|
-
title: "Delete
|
|
6241
|
-
description: "Permanently deletes a
|
|
6242
|
-
inputSchema: {
|
|
6243
|
-
schedule_id: z6.string().describe("ID of the schedule to delete")
|
|
6244
|
-
}
|
|
6245
|
-
},
|
|
6246
|
-
async (args) => {
|
|
6247
|
-
const start = Date.now();
|
|
6248
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6249
|
-
ctx.audit({
|
|
6250
|
-
tool_name: "ui_agent_schedule_delete",
|
|
6251
|
-
ocs_method: "[manus:schedule.delete]",
|
|
6252
|
-
status: "scope_denied",
|
|
6253
|
-
dry_run: false,
|
|
6254
|
-
duration_ms: 0
|
|
6255
|
-
});
|
|
6256
|
-
return scopeError("ui_agent_schedule_delete", "admin", ctx.props.scope);
|
|
6257
|
-
}
|
|
6258
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6259
|
-
return noManusKeyError2();
|
|
6260
|
-
}
|
|
6261
|
-
try {
|
|
6262
|
-
const result2 = await deleteSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
|
|
6263
|
-
ctx.audit({
|
|
6264
|
-
tool_name: "ui_agent_schedule_delete",
|
|
6265
|
-
ocs_method: "[manus:schedule.delete]",
|
|
6266
|
-
status: result2.ok ? "ok" : "error",
|
|
6267
|
-
dry_run: false,
|
|
6268
|
-
duration_ms: Date.now() - start,
|
|
6269
|
-
event_type: "ui_agent_dispatch"
|
|
6270
|
-
});
|
|
6271
|
-
if (!result2.ok) {
|
|
6272
|
-
return {
|
|
6273
|
-
isError: true,
|
|
6274
|
-
content: [
|
|
6275
|
-
{
|
|
6276
|
-
type: "text",
|
|
6277
|
-
text: JSON.stringify({
|
|
6278
|
-
error: "schedule_delete_failed",
|
|
6279
|
-
message: result2.error?.message ?? "Manus schedule.delete returned ok=false"
|
|
6280
|
-
})
|
|
6281
|
-
}
|
|
6282
|
-
]
|
|
6283
|
-
};
|
|
6284
|
-
}
|
|
6285
|
-
return {
|
|
6286
|
-
content: [
|
|
6287
|
-
{
|
|
6288
|
-
type: "text",
|
|
6289
|
-
text: JSON.stringify({ status: "deleted", schedule_id: args.schedule_id }, null, 2)
|
|
6290
|
-
}
|
|
6291
|
-
]
|
|
6292
|
-
};
|
|
6293
|
-
} catch (err7) {
|
|
6294
|
-
ctx.audit({
|
|
6295
|
-
tool_name: "ui_agent_schedule_delete",
|
|
6296
|
-
ocs_method: "[manus:schedule.delete]",
|
|
6297
|
-
status: "error",
|
|
6298
|
-
dry_run: false,
|
|
6299
|
-
duration_ms: Date.now() - start,
|
|
6300
|
-
event_type: "ui_agent_dispatch"
|
|
6301
|
-
});
|
|
6302
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6303
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6304
|
-
}
|
|
6305
|
-
}
|
|
6306
|
-
);
|
|
6307
|
-
server2.registerTool(
|
|
6308
|
-
"ui_agent_schedule_pause",
|
|
6309
|
-
{
|
|
6310
|
-
title: "Pause Manus Schedule",
|
|
6311
|
-
description: "Pauses an active Manus recurring schedule. The schedule is preserved and can be resumed later with ui_agent_schedule_resume. Requires admin scope.",
|
|
6312
|
-
inputSchema: {
|
|
6313
|
-
schedule_id: z6.string().describe("ID of the schedule to pause")
|
|
6314
|
-
}
|
|
6315
|
-
},
|
|
6316
|
-
async (args) => {
|
|
6317
|
-
const start = Date.now();
|
|
6318
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6319
|
-
ctx.audit({
|
|
6320
|
-
tool_name: "ui_agent_schedule_pause",
|
|
6321
|
-
ocs_method: "[manus:schedule.pause]",
|
|
6322
|
-
status: "scope_denied",
|
|
6323
|
-
dry_run: false,
|
|
6324
|
-
duration_ms: 0
|
|
6325
|
-
});
|
|
6326
|
-
return scopeError("ui_agent_schedule_pause", "admin", ctx.props.scope);
|
|
6327
|
-
}
|
|
6328
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6329
|
-
return noManusKeyError2();
|
|
6330
|
-
}
|
|
6331
|
-
try {
|
|
6332
|
-
const result2 = await pauseSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
|
|
6333
|
-
ctx.audit({
|
|
6334
|
-
tool_name: "ui_agent_schedule_pause",
|
|
6335
|
-
ocs_method: "[manus:schedule.pause]",
|
|
6336
|
-
status: result2.ok ? "ok" : "error",
|
|
6337
|
-
dry_run: false,
|
|
6338
|
-
duration_ms: Date.now() - start,
|
|
6339
|
-
event_type: "ui_agent_dispatch"
|
|
6340
|
-
});
|
|
6341
|
-
if (!result2.ok) {
|
|
6342
|
-
return {
|
|
6343
|
-
isError: true,
|
|
6344
|
-
content: [
|
|
6345
|
-
{
|
|
6346
|
-
type: "text",
|
|
6347
|
-
text: JSON.stringify({
|
|
6348
|
-
error: "schedule_pause_failed",
|
|
6349
|
-
message: result2.error?.message ?? "Manus schedule.pause returned ok=false"
|
|
6350
|
-
})
|
|
6351
|
-
}
|
|
6352
|
-
]
|
|
6353
|
-
};
|
|
6354
|
-
}
|
|
6355
|
-
return {
|
|
6356
|
-
content: [
|
|
6357
|
-
{
|
|
6358
|
-
type: "text",
|
|
6359
|
-
text: JSON.stringify({ status: "paused", schedule_id: args.schedule_id }, null, 2)
|
|
6360
|
-
}
|
|
6361
|
-
]
|
|
6362
|
-
};
|
|
6363
|
-
} catch (err7) {
|
|
6364
|
-
ctx.audit({
|
|
6365
|
-
tool_name: "ui_agent_schedule_pause",
|
|
6366
|
-
ocs_method: "[manus:schedule.pause]",
|
|
6367
|
-
status: "error",
|
|
6368
|
-
dry_run: false,
|
|
6369
|
-
duration_ms: Date.now() - start,
|
|
6370
|
-
event_type: "ui_agent_dispatch"
|
|
6371
|
-
});
|
|
6372
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6373
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6374
|
-
}
|
|
6375
|
-
}
|
|
6376
|
-
);
|
|
6377
|
-
server2.registerTool(
|
|
6378
|
-
"ui_agent_schedule_resume",
|
|
6379
|
-
{
|
|
6380
|
-
title: "Resume Manus Schedule",
|
|
6381
|
-
description: "Resumes a paused Manus recurring schedule. Requires admin scope.",
|
|
6382
|
-
inputSchema: {
|
|
6383
|
-
schedule_id: z6.string().describe("ID of the schedule to resume")
|
|
6384
|
-
}
|
|
6385
|
-
},
|
|
6386
|
-
async (args) => {
|
|
6387
|
-
const start = Date.now();
|
|
6388
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6389
|
-
ctx.audit({
|
|
6390
|
-
tool_name: "ui_agent_schedule_resume",
|
|
6391
|
-
ocs_method: "[manus:schedule.resume]",
|
|
6392
|
-
status: "scope_denied",
|
|
6393
|
-
dry_run: false,
|
|
6394
|
-
duration_ms: 0
|
|
6395
|
-
});
|
|
6396
|
-
return scopeError("ui_agent_schedule_resume", "admin", ctx.props.scope);
|
|
6397
|
-
}
|
|
6398
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6399
|
-
return noManusKeyError2();
|
|
6400
|
-
}
|
|
6401
|
-
try {
|
|
6402
|
-
const result2 = await resumeSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
|
|
6403
|
-
ctx.audit({
|
|
6404
|
-
tool_name: "ui_agent_schedule_resume",
|
|
6405
|
-
ocs_method: "[manus:schedule.resume]",
|
|
6406
|
-
status: result2.ok ? "ok" : "error",
|
|
6407
|
-
dry_run: false,
|
|
6408
|
-
duration_ms: Date.now() - start,
|
|
6409
|
-
event_type: "ui_agent_dispatch"
|
|
6410
|
-
});
|
|
6411
|
-
if (!result2.ok) {
|
|
6412
|
-
return {
|
|
6413
|
-
isError: true,
|
|
6414
|
-
content: [
|
|
6415
|
-
{
|
|
6416
|
-
type: "text",
|
|
6417
|
-
text: JSON.stringify({
|
|
6418
|
-
error: "schedule_resume_failed",
|
|
6419
|
-
message: result2.error?.message ?? "Manus schedule.resume returned ok=false"
|
|
6420
|
-
})
|
|
6421
|
-
}
|
|
6422
|
-
]
|
|
6423
|
-
};
|
|
6424
|
-
}
|
|
6425
|
-
return {
|
|
6426
|
-
content: [
|
|
6427
|
-
{
|
|
6428
|
-
type: "text",
|
|
6429
|
-
text: JSON.stringify({ status: "active", schedule_id: args.schedule_id }, null, 2)
|
|
6430
|
-
}
|
|
6431
|
-
]
|
|
6432
|
-
};
|
|
6433
|
-
} catch (err7) {
|
|
6434
|
-
ctx.audit({
|
|
6435
|
-
tool_name: "ui_agent_schedule_resume",
|
|
6436
|
-
ocs_method: "[manus:schedule.resume]",
|
|
6437
|
-
status: "error",
|
|
6438
|
-
dry_run: false,
|
|
6439
|
-
duration_ms: Date.now() - start,
|
|
6440
|
-
event_type: "ui_agent_dispatch"
|
|
6441
|
-
});
|
|
6442
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6443
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6312
|
+
server2.registerTool(
|
|
6313
|
+
"ui_agent_schedule_delete",
|
|
6314
|
+
{
|
|
6315
|
+
title: "Delete UI Agent Schedule",
|
|
6316
|
+
description: "Permanently deletes a recurring browser-agent schedule. This cannot be undone. Use ui_agent_schedule_pause to temporarily suspend instead. Requires admin scope." + STDIO_NOTE,
|
|
6317
|
+
inputSchema: {
|
|
6318
|
+
schedule_id: z6.string().describe("ID of the schedule to delete")
|
|
6444
6319
|
}
|
|
6445
|
-
}
|
|
6320
|
+
},
|
|
6321
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_delete", "admin") : uiAgentStubError("ui_agent_schedule_delete", "recurring_runtime")
|
|
6322
|
+
);
|
|
6323
|
+
server2.registerTool(
|
|
6324
|
+
"ui_agent_schedule_pause",
|
|
6325
|
+
{
|
|
6326
|
+
title: "Pause UI Agent Schedule",
|
|
6327
|
+
description: "Pauses an active recurring browser-agent schedule. The schedule is preserved and can be resumed later with ui_agent_schedule_resume. Requires admin scope." + STDIO_NOTE,
|
|
6328
|
+
inputSchema: {
|
|
6329
|
+
schedule_id: z6.string().describe("ID of the schedule to pause")
|
|
6330
|
+
}
|
|
6331
|
+
},
|
|
6332
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_pause", "admin") : uiAgentStubError("ui_agent_schedule_pause", "recurring_runtime")
|
|
6333
|
+
);
|
|
6334
|
+
server2.registerTool(
|
|
6335
|
+
"ui_agent_schedule_resume",
|
|
6336
|
+
{
|
|
6337
|
+
title: "Resume UI Agent Schedule",
|
|
6338
|
+
description: "Resumes a paused recurring browser-agent schedule. Requires admin scope." + STDIO_NOTE,
|
|
6339
|
+
inputSchema: {
|
|
6340
|
+
schedule_id: z6.string().describe("ID of the schedule to resume")
|
|
6341
|
+
}
|
|
6342
|
+
},
|
|
6343
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_resume", "admin") : uiAgentStubError("ui_agent_schedule_resume", "recurring_runtime")
|
|
6446
6344
|
);
|
|
6447
6345
|
server2.registerTool(
|
|
6448
6346
|
"ui_agent_usage",
|
|
6449
6347
|
{
|
|
6450
|
-
title: "
|
|
6451
|
-
description: "
|
|
6348
|
+
title: "UI Agent Usage & Spend",
|
|
6349
|
+
description: "Reports browser-agent spend for the current CLI session: runs dispatched, real vendor-billed cost in USD, and how many runs remain under the session limit. This is a per-process figure, not an account total \u2014 it resets when the CLI restarts, and it cannot see runs billed to the same API key by other processes or by the hosted Worker. Check your Browser Use account for the authoritative balance. Requires read scope.",
|
|
6452
6350
|
inputSchema: {}
|
|
6453
6351
|
},
|
|
6454
6352
|
async () => {
|
|
6455
|
-
const start = Date.now();
|
|
6456
6353
|
if (!ctx.props.scope.includes("read")) {
|
|
6457
6354
|
ctx.audit({
|
|
6458
6355
|
tool_name: "ui_agent_usage",
|
|
6459
|
-
ocs_method: "[
|
|
6356
|
+
ocs_method: "[ui-agent:usage]",
|
|
6460
6357
|
status: "scope_denied",
|
|
6461
6358
|
dry_run: false,
|
|
6462
6359
|
duration_ms: 0
|
|
6463
6360
|
});
|
|
6464
|
-
return scopeError("ui_agent_usage", "read", ctx.props.scope);
|
|
6465
|
-
}
|
|
6466
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6467
|
-
return noManusKeyError2();
|
|
6468
|
-
}
|
|
6469
|
-
try {
|
|
6470
|
-
const { data, from_cache } = await getUsage(ctx.env.MANUS_API_KEY, ctx.env);
|
|
6471
|
-
emitUsageThresholdIfNeeded(ctx.env, data);
|
|
6472
|
-
ctx.audit({
|
|
6473
|
-
tool_name: "ui_agent_usage",
|
|
6474
|
-
ocs_method: "[manus:usage.get]",
|
|
6475
|
-
status: "ok",
|
|
6476
|
-
dry_run: false,
|
|
6477
|
-
duration_ms: Date.now() - start
|
|
6478
|
-
});
|
|
6479
6361
|
return {
|
|
6362
|
+
isError: true,
|
|
6480
6363
|
content: [
|
|
6481
6364
|
{
|
|
6482
6365
|
type: "text",
|
|
6483
|
-
text:
|
|
6366
|
+
text: `Scope denied: tool 'ui_agent_usage' requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
6484
6367
|
}
|
|
6485
6368
|
]
|
|
6486
6369
|
};
|
|
6487
|
-
} catch (err7) {
|
|
6488
|
-
ctx.audit({
|
|
6489
|
-
tool_name: "ui_agent_usage",
|
|
6490
|
-
ocs_method: "[manus:usage.get]",
|
|
6491
|
-
status: "error",
|
|
6492
|
-
dry_run: false,
|
|
6493
|
-
duration_ms: Date.now() - start
|
|
6494
|
-
});
|
|
6495
|
-
const msg = err7 instanceof Error ? err7.message : String(err7);
|
|
6496
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6497
6370
|
}
|
|
6371
|
+
return {
|
|
6372
|
+
content: [
|
|
6373
|
+
{
|
|
6374
|
+
type: "text",
|
|
6375
|
+
text: JSON.stringify(
|
|
6376
|
+
{
|
|
6377
|
+
scope: "cli_session",
|
|
6378
|
+
configured: runtime.configured,
|
|
6379
|
+
runs_billed_this_session: runtime.spend.runs,
|
|
6380
|
+
cost_usd_this_session: runtime.spend.costUsd,
|
|
6381
|
+
runs_dispatched_this_session: runtime.budget.spent,
|
|
6382
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
6383
|
+
run_limit_env: MAX_RUNS_ENV,
|
|
6384
|
+
note: "Session-scoped. Resets when this CLI process exits and does not include runs billed to the same key elsewhere. The authoritative balance is in your Browser Use account."
|
|
6385
|
+
},
|
|
6386
|
+
null,
|
|
6387
|
+
2
|
|
6388
|
+
)
|
|
6389
|
+
}
|
|
6390
|
+
]
|
|
6391
|
+
};
|
|
6498
6392
|
}
|
|
6499
6393
|
);
|
|
6500
6394
|
}
|
|
@@ -6925,8 +6819,11 @@ var WALLET_TOOL_SCOPES = {
|
|
|
6925
6819
|
wallet_topup_checkout: "write",
|
|
6926
6820
|
wallet_auto_topup: "write"
|
|
6927
6821
|
};
|
|
6928
|
-
function
|
|
6929
|
-
return {
|
|
6822
|
+
function okStructured(payload) {
|
|
6823
|
+
return {
|
|
6824
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
6825
|
+
structuredContent: payload
|
|
6826
|
+
};
|
|
6930
6827
|
}
|
|
6931
6828
|
function err(text) {
|
|
6932
6829
|
return { isError: true, content: [{ type: "text", text }] };
|
|
@@ -6940,6 +6837,17 @@ function registerWalletTools(server2, ctx) {
|
|
|
6940
6837
|
title: "Wallet Balance",
|
|
6941
6838
|
description: "Read the caller organisation's Carrier prepaid wallet: current balance and auto-top-up settings. The wallet is what Carrier bills your own eSIM spend against \u2014 it is not an OCS balance and not your MCP plan credits. Params: none (the organisation is taken from the session). Returns: { org_id, balance_eur_cents, auto_topup_enabled, auto_topup_threshold_cents, auto_topup_pack_cents } \u2014 all amounts in EUR cents, not euros. Do NOT use this for a reseller account balance in OCS \u2014 use `get_reseller_info` or `list_reseller_accounts`. Do NOT use this for a subscriber's OCS balance \u2014 use `get_subscriber`. Do NOT use this for MCP plan credits \u2014 use `credit_balance`. Do NOT use this for money held at Stripe \u2014 use `stripe_connect_balance`.",
|
|
6942
6839
|
inputSchema: {},
|
|
6840
|
+
// Derived from the object literal this handler builds below, not from the
|
|
6841
|
+
// description. Every field is unconditional on the success path and comes
|
|
6842
|
+
// straight off the Wallet record (wallet-client.ts:31-34), so all five are
|
|
6843
|
+
// required rather than optional.
|
|
6844
|
+
outputSchema: {
|
|
6845
|
+
org_id: z7.string().describe("Carrier organisation the wallet belongs to"),
|
|
6846
|
+
balance_eur_cents: z7.number().int().describe("Current balance in EUR cents"),
|
|
6847
|
+
auto_topup_enabled: z7.boolean(),
|
|
6848
|
+
auto_topup_threshold_cents: z7.number().int().describe("Balance at or below which an auto top-up fires, EUR cents"),
|
|
6849
|
+
auto_topup_pack_cents: z7.number().int().describe("Amount an auto top-up charges, EUR cents")
|
|
6850
|
+
},
|
|
6943
6851
|
annotations: annotationsFor("wallet_balance", "read")
|
|
6944
6852
|
},
|
|
6945
6853
|
async () => {
|
|
@@ -6958,15 +6866,13 @@ function registerWalletTools(server2, ctx) {
|
|
|
6958
6866
|
sub: props2.sub,
|
|
6959
6867
|
reseller_id: props2.reseller_id
|
|
6960
6868
|
});
|
|
6961
|
-
return
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
})
|
|
6969
|
-
);
|
|
6869
|
+
return okStructured({
|
|
6870
|
+
org_id: orgId,
|
|
6871
|
+
balance_eur_cents: wallet.balance_eur_cents,
|
|
6872
|
+
auto_topup_enabled: wallet.auto_topup_enabled,
|
|
6873
|
+
auto_topup_threshold_cents: wallet.auto_topup_threshold_cents,
|
|
6874
|
+
auto_topup_pack_cents: wallet.auto_topup_pack_cents
|
|
6875
|
+
});
|
|
6970
6876
|
} catch (e) {
|
|
6971
6877
|
if (e instanceof WalletClientError) return err(`Wallet error: ${e.message}`);
|
|
6972
6878
|
Sentry2.captureException(e);
|
|
@@ -6984,6 +6890,31 @@ function registerWalletTools(server2, ctx) {
|
|
|
6984
6890
|
"Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
|
|
6985
6891
|
)
|
|
6986
6892
|
},
|
|
6893
|
+
/**
|
|
6894
|
+
* This tool really does return two different shapes, so the schema says
|
|
6895
|
+
* so rather than pretending otherwise. `pack` omitted returns the
|
|
6896
|
+
* catalogue arm (`packs`); `pack` supplied returns the session arm. An
|
|
6897
|
+
* `outputSchema` is an object shape, not a union type, so the two arms
|
|
6898
|
+
* are expressed as mutually exclusive optional groups and the pairing is
|
|
6899
|
+
* documented here. Marking either arm required would break the other.
|
|
6900
|
+
*/
|
|
6901
|
+
outputSchema: {
|
|
6902
|
+
packs: z7.array(
|
|
6903
|
+
z7.object({
|
|
6904
|
+
id: z7.string(),
|
|
6905
|
+
eur_cents: z7.number().int(),
|
|
6906
|
+
bonus_pct: z7.number(),
|
|
6907
|
+
bonus_eur_cents: z7.number().int(),
|
|
6908
|
+
credited_eur_cents: z7.number().int()
|
|
6909
|
+
})
|
|
6910
|
+
).optional().describe("Catalogue arm \u2014 present only when `pack` was omitted."),
|
|
6911
|
+
org_id: z7.string().optional().describe("Session arm."),
|
|
6912
|
+
pack_id: z7.string().optional().describe("Session arm."),
|
|
6913
|
+
pack_eur_cents: z7.number().int().optional().describe("Session arm."),
|
|
6914
|
+
bonus_eur_cents: z7.number().int().optional().describe("Session arm."),
|
|
6915
|
+
checkout_session_id: z7.string().optional().describe("Session arm."),
|
|
6916
|
+
checkout_url: z7.string().nullable().optional().describe("Session arm. Stripe may return null for a session URL.")
|
|
6917
|
+
},
|
|
6987
6918
|
annotations: annotationsFor("wallet_topup_checkout", "write")
|
|
6988
6919
|
},
|
|
6989
6920
|
async ({ pack }) => {
|
|
@@ -6992,17 +6923,15 @@ function registerWalletTools(server2, ctx) {
|
|
|
6992
6923
|
const orgId = defaultOrgId;
|
|
6993
6924
|
const start = Date.now();
|
|
6994
6925
|
if (!pack) {
|
|
6995
|
-
return
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
})
|
|
7005
|
-
);
|
|
6926
|
+
return okStructured({
|
|
6927
|
+
packs: PACKS.map((p) => ({
|
|
6928
|
+
id: p.id,
|
|
6929
|
+
eur_cents: p.eurCents,
|
|
6930
|
+
bonus_pct: p.bonusPct,
|
|
6931
|
+
bonus_eur_cents: bonusCentsFor(p),
|
|
6932
|
+
credited_eur_cents: p.eurCents + bonusCentsFor(p)
|
|
6933
|
+
}))
|
|
6934
|
+
});
|
|
7006
6935
|
}
|
|
7007
6936
|
const selected = resolvePack(pack);
|
|
7008
6937
|
if (!selected) {
|
|
@@ -7021,16 +6950,14 @@ function registerWalletTools(server2, ctx) {
|
|
|
7021
6950
|
sub: props2.sub,
|
|
7022
6951
|
reseller_id: props2.reseller_id
|
|
7023
6952
|
});
|
|
7024
|
-
return
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
})
|
|
7033
|
-
);
|
|
6953
|
+
return okStructured({
|
|
6954
|
+
org_id: orgId,
|
|
6955
|
+
pack_id: selected.id,
|
|
6956
|
+
pack_eur_cents: selected.eurCents,
|
|
6957
|
+
bonus_eur_cents: bonusCentsFor(selected),
|
|
6958
|
+
checkout_session_id: session.id,
|
|
6959
|
+
checkout_url: session.url
|
|
6960
|
+
});
|
|
7034
6961
|
} catch (e) {
|
|
7035
6962
|
Sentry2.captureException(e);
|
|
7036
6963
|
return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7045,6 +6972,26 @@ function registerWalletTools(server2, ctx) {
|
|
|
7045
6972
|
inputSchema: {
|
|
7046
6973
|
pack_cents: z7.number().int().positive().optional().describe("Override pack amount in EUR cents (must match a catalog pack).")
|
|
7047
6974
|
},
|
|
6975
|
+
/**
|
|
6976
|
+
* Only the charged arm is described here — `charged: false` returns
|
|
6977
|
+
* through `err()`, and the SDK skips output validation on an `isError`
|
|
6978
|
+
* result.
|
|
6979
|
+
*
|
|
6980
|
+
* `balance_after_cents` is optional and the description above is wrong to
|
|
6981
|
+
* promise it. runAutoTopup has a second success arm (wallet-tools.ts:337)
|
|
6982
|
+
* where the card was charged but crediting Atlas failed: it carries
|
|
6983
|
+
* `paymentIntentId` and a `reason`, and no balance. Requiring the field
|
|
6984
|
+
* would turn that arm — a real charge the caller must be told about —
|
|
6985
|
+
* into a validation error.
|
|
6986
|
+
*/
|
|
6987
|
+
outputSchema: {
|
|
6988
|
+
org_id: z7.string(),
|
|
6989
|
+
charged: z7.literal(true),
|
|
6990
|
+
credited_eur_cents: z7.number().int(),
|
|
6991
|
+
balance_after_cents: z7.number().int().optional().describe("Absent when the charge succeeded but crediting is pending."),
|
|
6992
|
+
payment_intent_id: z7.string().optional(),
|
|
6993
|
+
note: z7.string().optional().describe("Present when the charge landed but the credit is still pending.")
|
|
6994
|
+
},
|
|
7048
6995
|
annotations: annotationsFor("wallet_auto_topup", "write")
|
|
7049
6996
|
},
|
|
7050
6997
|
async ({ pack_cents }) => {
|
|
@@ -7068,16 +7015,18 @@ function registerWalletTools(server2, ctx) {
|
|
|
7068
7015
|
JSON.stringify({ org_id: orgId, charged: false, reason: result2.reason })
|
|
7069
7016
|
);
|
|
7070
7017
|
}
|
|
7071
|
-
return
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
}
|
|
7080
|
-
|
|
7018
|
+
return okStructured({
|
|
7019
|
+
org_id: orgId,
|
|
7020
|
+
charged: true,
|
|
7021
|
+
credited_eur_cents: result2.creditedCents,
|
|
7022
|
+
// Spread rather than assign, so the key is absent instead of set to
|
|
7023
|
+
// `undefined`. JSON.stringify drops an undefined value anyway, so
|
|
7024
|
+
// assigning it would make `content` and `structuredContent` disagree
|
|
7025
|
+
// about whether the field is there.
|
|
7026
|
+
...result2.balanceAfterCents !== void 0 ? { balance_after_cents: result2.balanceAfterCents } : {},
|
|
7027
|
+
...result2.paymentIntentId !== void 0 ? { payment_intent_id: result2.paymentIntentId } : {},
|
|
7028
|
+
...result2.reason ? { note: result2.reason } : {}
|
|
7029
|
+
});
|
|
7081
7030
|
} catch (e) {
|
|
7082
7031
|
Sentry2.captureException(e);
|
|
7083
7032
|
return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7103,25 +7052,30 @@ async function issueConfirmToken(env, sub, toolName) {
|
|
|
7103
7052
|
await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
|
|
7104
7053
|
return token2;
|
|
7105
7054
|
}
|
|
7106
|
-
function
|
|
7055
|
+
function ok(text) {
|
|
7107
7056
|
return { content: [{ type: "text", text }] };
|
|
7108
7057
|
}
|
|
7109
7058
|
function err2(text) {
|
|
7110
7059
|
return { isError: true, content: [{ type: "text", text }] };
|
|
7111
7060
|
}
|
|
7112
7061
|
async function stripeGet(stripeKey, path, connectedAccountId) {
|
|
7113
|
-
const headers = {
|
|
7062
|
+
const headers = {
|
|
7063
|
+
Authorization: `Bearer ${stripeKey}`,
|
|
7064
|
+
"Stripe-Version": "2025-08-27.basil"
|
|
7065
|
+
};
|
|
7114
7066
|
if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
|
|
7115
7067
|
const res = await fetch(`https://api.stripe.com${path}`, { headers });
|
|
7116
7068
|
const data = await res.json();
|
|
7117
7069
|
return { ok: res.ok, status: res.status, data };
|
|
7118
7070
|
}
|
|
7119
|
-
async function stripePost(stripeKey, path, params, connectedAccountId) {
|
|
7071
|
+
async function stripePost(stripeKey, path, params, connectedAccountId, idempotencyKey) {
|
|
7120
7072
|
const headers = {
|
|
7121
7073
|
Authorization: `Bearer ${stripeKey}`,
|
|
7074
|
+
"Stripe-Version": "2025-08-27.basil",
|
|
7122
7075
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
7123
7076
|
};
|
|
7124
7077
|
if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
|
|
7078
|
+
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
7125
7079
|
const res = await fetch(`https://api.stripe.com${path}`, {
|
|
7126
7080
|
method: "POST",
|
|
7127
7081
|
headers,
|
|
@@ -7130,6 +7084,9 @@ async function stripePost(stripeKey, path, params, connectedAccountId) {
|
|
|
7130
7084
|
const data = await res.json();
|
|
7131
7085
|
return { ok: res.ok, status: res.status, data };
|
|
7132
7086
|
}
|
|
7087
|
+
function refundIdempotencyKey(sub, charge, amount, reason) {
|
|
7088
|
+
return `carrier-refund:${sub}:${charge}:${amount ?? "full"}:${reason ?? "none"}`;
|
|
7089
|
+
}
|
|
7133
7090
|
var STRIPE_CONNECT_TOOL_SCOPES = {
|
|
7134
7091
|
stripe_connect_status: "read",
|
|
7135
7092
|
stripe_connect_payouts: "read",
|
|
@@ -7177,7 +7134,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7177
7134
|
if (!stripeKey) return err2("Stripe not configured");
|
|
7178
7135
|
const accountId = await env.CARRIER_USERS.get(opKvKey);
|
|
7179
7136
|
if (!accountId) {
|
|
7180
|
-
return
|
|
7137
|
+
return ok(JSON.stringify({ status: "not_connected", operator_id: opId }));
|
|
7181
7138
|
}
|
|
7182
7139
|
const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
|
|
7183
7140
|
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
@@ -7192,7 +7149,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7192
7149
|
sub: props2.sub,
|
|
7193
7150
|
reseller_id: props2.reseller_id
|
|
7194
7151
|
});
|
|
7195
|
-
return
|
|
7152
|
+
return ok(JSON.stringify({
|
|
7196
7153
|
status,
|
|
7197
7154
|
operator_id: opId,
|
|
7198
7155
|
account_id: accountId,
|
|
@@ -7243,7 +7200,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7243
7200
|
sub: props2.sub,
|
|
7244
7201
|
reseller_id: props2.reseller_id
|
|
7245
7202
|
});
|
|
7246
|
-
return
|
|
7203
|
+
return ok(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
|
|
7247
7204
|
} catch (e) {
|
|
7248
7205
|
Sentry3.captureException(e);
|
|
7249
7206
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7278,7 +7235,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7278
7235
|
sub: props2.sub,
|
|
7279
7236
|
reseller_id: props2.reseller_id
|
|
7280
7237
|
});
|
|
7281
|
-
return
|
|
7238
|
+
return ok(JSON.stringify({
|
|
7282
7239
|
account_id: accountId,
|
|
7283
7240
|
available: result2.data.available ?? [],
|
|
7284
7241
|
pending: result2.data.pending ?? []
|
|
@@ -7309,7 +7266,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7309
7266
|
const toolName = "stripe_connect_refund";
|
|
7310
7267
|
if (!confirm_token) {
|
|
7311
7268
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7312
|
-
return
|
|
7269
|
+
return ok(
|
|
7313
7270
|
`HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
|
|
7314
7271
|
confirm_token: ${token2}
|
|
7315
7272
|
Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
|
|
@@ -7327,7 +7284,13 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7327
7284
|
const params = { charge: charge_id };
|
|
7328
7285
|
if (amount_cents) params.amount = String(amount_cents);
|
|
7329
7286
|
if (reason) params.reason = reason;
|
|
7330
|
-
const result2 = await stripePost(
|
|
7287
|
+
const result2 = await stripePost(
|
|
7288
|
+
stripeKey,
|
|
7289
|
+
"/v1/refunds",
|
|
7290
|
+
params,
|
|
7291
|
+
accountId,
|
|
7292
|
+
refundIdempotencyKey(props2.sub, charge_id, amount_cents, reason)
|
|
7293
|
+
);
|
|
7331
7294
|
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
7332
7295
|
writeAudit(env, {
|
|
7333
7296
|
tool_name: toolName,
|
|
@@ -7338,7 +7301,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7338
7301
|
sub: props2.sub,
|
|
7339
7302
|
reseller_id: props2.reseller_id
|
|
7340
7303
|
});
|
|
7341
|
-
return
|
|
7304
|
+
return ok(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
|
|
7342
7305
|
} catch (e) {
|
|
7343
7306
|
Sentry3.captureException(e);
|
|
7344
7307
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7378,7 +7341,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7378
7341
|
sub: props2.sub,
|
|
7379
7342
|
reseller_id: props2.reseller_id
|
|
7380
7343
|
});
|
|
7381
|
-
return
|
|
7344
|
+
return ok(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
|
|
7382
7345
|
} catch (e) {
|
|
7383
7346
|
Sentry3.captureException(e);
|
|
7384
7347
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7421,7 +7384,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7421
7384
|
sub: props2.sub,
|
|
7422
7385
|
reseller_id: props2.reseller_id
|
|
7423
7386
|
});
|
|
7424
|
-
return
|
|
7387
|
+
return ok(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
|
|
7425
7388
|
} catch (e) {
|
|
7426
7389
|
Sentry3.captureException(e);
|
|
7427
7390
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7451,7 +7414,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7451
7414
|
const start = Date.now();
|
|
7452
7415
|
if (!confirm_token) {
|
|
7453
7416
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7454
|
-
return
|
|
7417
|
+
return ok(
|
|
7455
7418
|
`HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
|
|
7456
7419
|
confirm_token: ${token2}
|
|
7457
7420
|
Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
@@ -7477,7 +7440,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
7477
7440
|
sub: props2.sub,
|
|
7478
7441
|
reseller_id: props2.reseller_id
|
|
7479
7442
|
});
|
|
7480
|
-
return
|
|
7443
|
+
return ok(`Review ${review_id} approved. Charge will proceed.`);
|
|
7481
7444
|
} catch (e) {
|
|
7482
7445
|
Sentry3.captureException(e);
|
|
7483
7446
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7507,7 +7470,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
7507
7470
|
const start = Date.now();
|
|
7508
7471
|
if (!confirm_token) {
|
|
7509
7472
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7510
|
-
return
|
|
7473
|
+
return ok(
|
|
7511
7474
|
`HARD_BLOCK: Declining review ${review_id} will close/block the charge.
|
|
7512
7475
|
confirm_token: ${token2}
|
|
7513
7476
|
Expires in 5 minutes.`
|
|
@@ -7533,7 +7496,7 @@ Expires in 5 minutes.`
|
|
|
7533
7496
|
sub: props2.sub,
|
|
7534
7497
|
reseller_id: props2.reseller_id
|
|
7535
7498
|
});
|
|
7536
|
-
return
|
|
7499
|
+
return ok(`Review ${review_id} declined.`);
|
|
7537
7500
|
} catch (e) {
|
|
7538
7501
|
Sentry3.captureException(e);
|
|
7539
7502
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7564,7 +7527,7 @@ Expires in 5 minutes.`
|
|
|
7564
7527
|
const start = Date.now();
|
|
7565
7528
|
if (!confirm_token) {
|
|
7566
7529
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7567
|
-
return
|
|
7530
|
+
return ok(
|
|
7568
7531
|
`HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
|
|
7569
7532
|
confirm_token: ${token2}
|
|
7570
7533
|
Expires in 5 minutes.`
|
|
@@ -7590,7 +7553,7 @@ Expires in 5 minutes.`
|
|
|
7590
7553
|
sub: props2.sub,
|
|
7591
7554
|
reseller_id: props2.reseller_id
|
|
7592
7555
|
});
|
|
7593
|
-
return
|
|
7556
|
+
return ok(`Added "${value}" to Radar list ${value_list_id}.`);
|
|
7594
7557
|
} catch (e) {
|
|
7595
7558
|
Sentry3.captureException(e);
|
|
7596
7559
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7616,7 +7579,7 @@ Expires in 5 minutes.`
|
|
|
7616
7579
|
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7617
7580
|
);
|
|
7618
7581
|
}
|
|
7619
|
-
return
|
|
7582
|
+
return ok(
|
|
7620
7583
|
`Stripe Radar does not expose rule enable/disable via the public API.
|
|
7621
7584
|
To ${enabled ? "enable" : "disable"} rule ${rule_id}:
|
|
7622
7585
|
1. Open https://dashboard.stripe.com/radar/rules
|
|
@@ -7633,9 +7596,9 @@ import { z as z9 } from "zod";
|
|
|
7633
7596
|
var STOREFRONT_LOGO_TOOL_SCOPES = {
|
|
7634
7597
|
generate_storefront_logo: "read"
|
|
7635
7598
|
};
|
|
7636
|
-
var
|
|
7599
|
+
var ok2 = (text) => ({ content: [{ type: "text", text }] });
|
|
7637
7600
|
var err3 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
7638
|
-
function registerStorefrontLogoTools(server2, env = {}) {
|
|
7601
|
+
function registerStorefrontLogoTools(server2, env = {}, props2) {
|
|
7639
7602
|
server2.registerTool(
|
|
7640
7603
|
"generate_storefront_logo",
|
|
7641
7604
|
{
|
|
@@ -7649,6 +7612,8 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
7649
7612
|
annotations: annotationsFor("generate_storefront_logo", "read")
|
|
7650
7613
|
},
|
|
7651
7614
|
async ({ name, accent, tagline }) => {
|
|
7615
|
+
const denied = denyUnlessScoped(props2, "generate_storefront_logo", STOREFRONT_LOGO_TOOL_SCOPES);
|
|
7616
|
+
if (denied) return denied;
|
|
7652
7617
|
try {
|
|
7653
7618
|
const hex = accent ? accent.startsWith("#") ? accent : `#${accent}` : void 0;
|
|
7654
7619
|
const logo = await generateStorefrontLogo({
|
|
@@ -7657,7 +7622,7 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
7657
7622
|
tagline,
|
|
7658
7623
|
env: { ...env, ...process.env }
|
|
7659
7624
|
});
|
|
7660
|
-
return
|
|
7625
|
+
return ok2(
|
|
7661
7626
|
JSON.stringify({
|
|
7662
7627
|
brand: name,
|
|
7663
7628
|
source: logo.source,
|
|
@@ -7682,11 +7647,11 @@ var STOREFRONT_DEPLOY_TOOL_SCOPES = {
|
|
|
7682
7647
|
deploy_storefront: "write",
|
|
7683
7648
|
provision_storefront_clerk: "write"
|
|
7684
7649
|
};
|
|
7685
|
-
var
|
|
7650
|
+
var ok3 = (value) => ({
|
|
7686
7651
|
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
7687
7652
|
});
|
|
7688
7653
|
var err4 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
7689
|
-
function registerStorefrontDeployTools(server2) {
|
|
7654
|
+
function registerStorefrontDeployTools(server2, props2) {
|
|
7690
7655
|
server2.registerTool(
|
|
7691
7656
|
"list_deploy_targets",
|
|
7692
7657
|
{
|
|
@@ -7698,10 +7663,12 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7698
7663
|
annotations: annotationsFor("list_deploy_targets", "read")
|
|
7699
7664
|
},
|
|
7700
7665
|
async ({ dir }) => {
|
|
7666
|
+
const denied = denyUnlessScoped(props2, "list_deploy_targets", STOREFRONT_DEPLOY_TOOL_SCOPES);
|
|
7667
|
+
if (denied) return denied;
|
|
7701
7668
|
try {
|
|
7702
7669
|
const statuses = await probeAll(dir);
|
|
7703
7670
|
const ranked = rankTargets(statuses);
|
|
7704
|
-
return
|
|
7671
|
+
return ok3({
|
|
7705
7672
|
targets: statuses,
|
|
7706
7673
|
default: ranked[0]?.id ?? null,
|
|
7707
7674
|
reason: ranked.length ? void 0 : "No host is installed and logged in."
|
|
@@ -7728,6 +7695,8 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7728
7695
|
annotations: annotationsFor("deploy_storefront", "write")
|
|
7729
7696
|
},
|
|
7730
7697
|
async ({ dir, target, name, skip_build, push_secrets, verify, domain }) => {
|
|
7698
|
+
const denied = denyUnlessScoped(props2, "deploy_storefront", STOREFRONT_DEPLOY_TOOL_SCOPES);
|
|
7699
|
+
if (denied) return denied;
|
|
7731
7700
|
try {
|
|
7732
7701
|
if (target && !isTargetId(target)) return err4(`Unknown target "${target}".`);
|
|
7733
7702
|
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
@@ -7747,7 +7716,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7747
7716
|
return err4(result2.reason ?? "Deploy failed.");
|
|
7748
7717
|
}
|
|
7749
7718
|
const verification = verify !== false && result2.url ? await verifyStorefront(result2.url, dir, process.env) : void 0;
|
|
7750
|
-
return
|
|
7719
|
+
return ok3({
|
|
7751
7720
|
deployed: true,
|
|
7752
7721
|
target: result2.target,
|
|
7753
7722
|
project: result2.projectName,
|
|
@@ -7780,6 +7749,12 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7780
7749
|
annotations: annotationsFor("provision_storefront_clerk", "write")
|
|
7781
7750
|
},
|
|
7782
7751
|
async ({ dir, name, production, no_create, url }) => {
|
|
7752
|
+
const denied = denyUnlessScoped(
|
|
7753
|
+
props2,
|
|
7754
|
+
"provision_storefront_clerk",
|
|
7755
|
+
STOREFRONT_DEPLOY_TOOL_SCOPES
|
|
7756
|
+
);
|
|
7757
|
+
if (denied) return denied;
|
|
7783
7758
|
try {
|
|
7784
7759
|
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
7785
7760
|
const result2 = await provisionClerk({
|
|
@@ -7792,7 +7767,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7792
7767
|
cli: clerkCliDeps()
|
|
7793
7768
|
});
|
|
7794
7769
|
if (!result2.ok || !result2.credentials) {
|
|
7795
|
-
return
|
|
7770
|
+
return ok3({
|
|
7796
7771
|
provisioned: false,
|
|
7797
7772
|
tier: result2.tier,
|
|
7798
7773
|
reason: result2.reason,
|
|
@@ -7808,7 +7783,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7808
7783
|
allowedOrigins,
|
|
7809
7784
|
redirectUrls
|
|
7810
7785
|
});
|
|
7811
|
-
return
|
|
7786
|
+
return ok3({
|
|
7812
7787
|
provisioned: true,
|
|
7813
7788
|
tier: result2.tier,
|
|
7814
7789
|
application_id: result2.credentials.applicationId ?? null,
|
|
@@ -10415,7 +10390,7 @@ function registerRateLimitStatusTool(server2, ctx) {
|
|
|
10415
10390
|
|
|
10416
10391
|
// src/tools-depletion-events.ts
|
|
10417
10392
|
import { z as z14 } from "zod";
|
|
10418
|
-
function
|
|
10393
|
+
function ok4(payload) {
|
|
10419
10394
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
10420
10395
|
}
|
|
10421
10396
|
function err5(message) {
|
|
@@ -10475,7 +10450,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10475
10450
|
if (subscriberId) {
|
|
10476
10451
|
const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
|
|
10477
10452
|
if (!raw) {
|
|
10478
|
-
return
|
|
10453
|
+
return ok4({
|
|
10479
10454
|
subscriber_id: subscriberId,
|
|
10480
10455
|
depleted: false,
|
|
10481
10456
|
event: null,
|
|
@@ -10486,7 +10461,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10486
10461
|
try {
|
|
10487
10462
|
event = JSON.parse(raw);
|
|
10488
10463
|
} catch {
|
|
10489
|
-
return
|
|
10464
|
+
return ok4({
|
|
10490
10465
|
subscriber_id: subscriberId,
|
|
10491
10466
|
depleted: false,
|
|
10492
10467
|
event: null,
|
|
@@ -10494,7 +10469,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10494
10469
|
});
|
|
10495
10470
|
}
|
|
10496
10471
|
if (!await ownsEvent(ctx, scopeToken, event.iccid)) {
|
|
10497
|
-
return
|
|
10472
|
+
return ok4({
|
|
10498
10473
|
subscriber_id: subscriberId,
|
|
10499
10474
|
depleted: false,
|
|
10500
10475
|
event: null,
|
|
@@ -10502,14 +10477,14 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10502
10477
|
});
|
|
10503
10478
|
}
|
|
10504
10479
|
if (since && event.depletedAt < since) {
|
|
10505
|
-
return
|
|
10480
|
+
return ok4({
|
|
10506
10481
|
subscriber_id: subscriberId,
|
|
10507
10482
|
depleted: false,
|
|
10508
10483
|
event: null,
|
|
10509
10484
|
note: `No bundle depletion after ${since}.`
|
|
10510
10485
|
});
|
|
10511
10486
|
}
|
|
10512
|
-
return
|
|
10487
|
+
return ok4({
|
|
10513
10488
|
subscriber_id: subscriberId,
|
|
10514
10489
|
depleted: true,
|
|
10515
10490
|
event: {
|
|
@@ -10523,7 +10498,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10523
10498
|
}
|
|
10524
10499
|
const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
|
|
10525
10500
|
if (listResult.keys.length === 0) {
|
|
10526
|
-
return
|
|
10501
|
+
return ok4({
|
|
10527
10502
|
depletions: [],
|
|
10528
10503
|
total: 0,
|
|
10529
10504
|
note: "No bundle depletion events in the last 7 days."
|
|
@@ -10550,7 +10525,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10550
10525
|
})
|
|
10551
10526
|
);
|
|
10552
10527
|
events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
|
|
10553
|
-
return
|
|
10528
|
+
return ok4({
|
|
10554
10529
|
depletions: events,
|
|
10555
10530
|
total: events.length,
|
|
10556
10531
|
list_truncated: !listResult.list_complete
|
|
@@ -11926,7 +11901,7 @@ function resolvePortalBaseUrl(env) {
|
|
|
11926
11901
|
function getGreenzoneKv(env) {
|
|
11927
11902
|
return env.GREENZONE_STATE_KV ?? null;
|
|
11928
11903
|
}
|
|
11929
|
-
function
|
|
11904
|
+
function ok5(payload) {
|
|
11930
11905
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
11931
11906
|
}
|
|
11932
11907
|
function err6(message) {
|
|
@@ -11962,7 +11937,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11962
11937
|
...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
|
|
11963
11938
|
];
|
|
11964
11939
|
if (dry_run) {
|
|
11965
|
-
return
|
|
11940
|
+
return ok5({
|
|
11966
11941
|
dry_run: true,
|
|
11967
11942
|
message: "Would execute the following Kapture steps against the OCS portal:",
|
|
11968
11943
|
kapture_steps: steps,
|
|
@@ -11985,7 +11960,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11985
11960
|
} else {
|
|
11986
11961
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
11987
11962
|
}
|
|
11988
|
-
return
|
|
11963
|
+
return ok5({
|
|
11989
11964
|
requires_kapture: true,
|
|
11990
11965
|
message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
11991
11966
|
kapture_steps: steps,
|
|
@@ -12023,7 +11998,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12023
11998
|
...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
|
|
12024
11999
|
];
|
|
12025
12000
|
if (dry_run) {
|
|
12026
|
-
return
|
|
12001
|
+
return ok5({
|
|
12027
12002
|
dry_run: true,
|
|
12028
12003
|
message: "Would execute the following Kapture steps to remove from OCS portal:",
|
|
12029
12004
|
kapture_steps: steps,
|
|
@@ -12046,7 +12021,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12046
12021
|
} else {
|
|
12047
12022
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
12048
12023
|
}
|
|
12049
|
-
return
|
|
12024
|
+
return ok5({
|
|
12050
12025
|
requires_kapture: true,
|
|
12051
12026
|
message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
12052
12027
|
kapture_steps: steps,
|
|
@@ -12077,7 +12052,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12077
12052
|
const { from_portal } = args;
|
|
12078
12053
|
if (from_portal) {
|
|
12079
12054
|
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
12080
|
-
return
|
|
12055
|
+
return ok5({
|
|
12081
12056
|
requires_kapture: true,
|
|
12082
12057
|
message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
|
|
12083
12058
|
kapture_steps: buildListSteps(portalBaseUrl),
|
|
@@ -12086,7 +12061,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12086
12061
|
}
|
|
12087
12062
|
const kv = getGreenzoneKv(ctx.env);
|
|
12088
12063
|
if (!kv) {
|
|
12089
|
-
return
|
|
12064
|
+
return ok5({
|
|
12090
12065
|
source: "kv_cache",
|
|
12091
12066
|
hosts: [],
|
|
12092
12067
|
ips: [],
|
|
@@ -12098,7 +12073,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12098
12073
|
}
|
|
12099
12074
|
try {
|
|
12100
12075
|
const state = await readState(kv);
|
|
12101
|
-
return
|
|
12076
|
+
return ok5({
|
|
12102
12077
|
source: "kv_cache",
|
|
12103
12078
|
hosts: state.hosts,
|
|
12104
12079
|
ips: state.ips,
|
|
@@ -12115,43 +12090,228 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12115
12090
|
|
|
12116
12091
|
// src/tools-ui-agent-generic.ts
|
|
12117
12092
|
import { z as z22 } from "zod";
|
|
12118
|
-
var
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12093
|
+
var COST_PER_STEP_USD = 0.014;
|
|
12094
|
+
var SESSION_OVERHEAD_USD = 5e-3;
|
|
12095
|
+
function estimateCostUsd2(steps) {
|
|
12096
|
+
return Math.round((COST_PER_STEP_USD * steps + SESSION_OVERHEAD_USD) * 100) / 100;
|
|
12097
|
+
}
|
|
12098
|
+
function notConfiguredError(toolName) {
|
|
12099
|
+
return {
|
|
12100
|
+
isError: true,
|
|
12101
|
+
content: [
|
|
12102
|
+
{
|
|
12103
|
+
type: "text",
|
|
12104
|
+
text: JSON.stringify({
|
|
12105
|
+
error: "browser_use_not_configured",
|
|
12106
|
+
tool: toolName,
|
|
12107
|
+
message: "BROWSER_USE_API_KEY is not set, so the browsing agent cannot run. Set it in the environment of the process running this MCP server. Runs are billed to that key at roughly $0.20-1.00 each."
|
|
12108
|
+
})
|
|
12109
|
+
}
|
|
12110
|
+
]
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
function scopeError2(toolName, required, actual) {
|
|
12114
|
+
return {
|
|
12115
|
+
isError: true,
|
|
12116
|
+
content: [
|
|
12117
|
+
{
|
|
12118
|
+
type: "text",
|
|
12119
|
+
text: JSON.stringify({
|
|
12120
|
+
error: "scope_denied",
|
|
12121
|
+
message: `Scope denied: tool '${toolName}' requires '${required}' scope. Your token has: [${actual.join(", ")}].`
|
|
12122
|
+
})
|
|
12123
|
+
}
|
|
12124
|
+
]
|
|
12125
|
+
};
|
|
12126
|
+
}
|
|
12127
|
+
function registerUiAgentGenericTools(server2, ctx, runtime) {
|
|
12132
12128
|
server2.registerTool(
|
|
12133
12129
|
"ui_agent_ask",
|
|
12134
12130
|
{
|
|
12135
|
-
title: "Dispatch Generic
|
|
12136
|
-
description: "
|
|
12131
|
+
title: "Dispatch Generic Browsing Agent",
|
|
12132
|
+
description: "Send a plain-language task to a hosted browsing agent, which drives a real browser on the open web \u2014 navigating pages, searching, extracting content, filling forms outside the OCS portal. Each run costs roughly $0.20-1.00 in model spend against your own BROWSER_USE_API_KEY, so treat it as the expensive last resort, not a lookup. Limited to CARRIER_BROWSER_USE_MAX_RUNS runs per CLI session (default 10). Params: `prompt` (1-4000 chars \u2014 name the target URL and the exact data or action), `max_steps` (1-30, default 15; prices the dry-run estimate only), `dry_run` (true returns only `estimated_cost_usd` and creates no session). Returns the finished result: unlike the hosted Worker, a local CLI has no background runtime, so this call blocks for the length of the run. Requires `write` scope. Do NOT use this to work out which Carrier tool answers a request \u2014 that is `carrier_ask`, which costs nothing. Do NOT use this for anything the OCS API already covers; every other tool here is faster, free, and audited.",
|
|
12137
12133
|
inputSchema: {
|
|
12138
|
-
prompt: z22.string().min(1).max(4e3).describe(
|
|
12139
|
-
|
|
12140
|
-
|
|
12134
|
+
prompt: z22.string().min(1).max(4e3).describe(
|
|
12135
|
+
"Natural-language task description for the browsing agent. Be specific: include target URLs, data to extract, or actions to perform."
|
|
12136
|
+
),
|
|
12137
|
+
max_steps: z22.number().int().min(1).max(30).optional().describe(
|
|
12138
|
+
"Advisory step budget (1\u201330, default 15). Prices the dry-run estimate; the hosted agent sets its own depth."
|
|
12139
|
+
),
|
|
12140
|
+
dry_run: z22.boolean().optional().describe(
|
|
12141
|
+
"Preview cost estimate without dispatching. Returns estimated_cost_usd only \u2014 no browser session is created."
|
|
12142
|
+
)
|
|
12141
12143
|
}
|
|
12142
12144
|
},
|
|
12143
|
-
async (
|
|
12145
|
+
async (args) => {
|
|
12146
|
+
const start = Date.now();
|
|
12147
|
+
const toolName = "ui_agent_ask";
|
|
12148
|
+
const { prompt, max_steps = 15, dry_run = false } = args;
|
|
12149
|
+
if (!ctx.props.scope.includes("write") && !ctx.props.scope.includes("admin")) {
|
|
12150
|
+
ctx.audit({
|
|
12151
|
+
tool_name: toolName,
|
|
12152
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12153
|
+
status: "scope_denied",
|
|
12154
|
+
dry_run: false,
|
|
12155
|
+
duration_ms: 0
|
|
12156
|
+
});
|
|
12157
|
+
return scopeError2(toolName, "write", ctx.props.scope);
|
|
12158
|
+
}
|
|
12159
|
+
if (dry_run) {
|
|
12160
|
+
return {
|
|
12161
|
+
content: [
|
|
12162
|
+
{
|
|
12163
|
+
type: "text",
|
|
12164
|
+
text: JSON.stringify(
|
|
12165
|
+
{
|
|
12166
|
+
status: "dry_run",
|
|
12167
|
+
estimated_cost_usd: estimateCostUsd2(max_steps),
|
|
12168
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
12169
|
+
note: "No browser session was created and nothing was billed."
|
|
12170
|
+
},
|
|
12171
|
+
null,
|
|
12172
|
+
2
|
|
12173
|
+
)
|
|
12174
|
+
}
|
|
12175
|
+
]
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
12179
|
+
return notConfiguredError(toolName);
|
|
12180
|
+
}
|
|
12181
|
+
try {
|
|
12182
|
+
runtime.budget.reserve();
|
|
12183
|
+
} catch (err7) {
|
|
12184
|
+
if (err7 instanceof SpendLimitError) {
|
|
12185
|
+
ctx.audit({
|
|
12186
|
+
tool_name: toolName,
|
|
12187
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12188
|
+
// The existing quota status, not a new one: AuditRow's status is a
|
|
12189
|
+
// closed union shared with the Worker, and a stdio-only value would
|
|
12190
|
+
// be a row shape the hosted surface never emits.
|
|
12191
|
+
status: "quota_exceeded",
|
|
12192
|
+
dry_run: false,
|
|
12193
|
+
duration_ms: 0,
|
|
12194
|
+
event_type: "ui_agent_dispatch"
|
|
12195
|
+
});
|
|
12196
|
+
return {
|
|
12197
|
+
isError: true,
|
|
12198
|
+
content: [
|
|
12199
|
+
{
|
|
12200
|
+
type: "text",
|
|
12201
|
+
text: JSON.stringify({
|
|
12202
|
+
error: "run_limit_reached",
|
|
12203
|
+
message: err7.message
|
|
12204
|
+
})
|
|
12205
|
+
}
|
|
12206
|
+
]
|
|
12207
|
+
};
|
|
12208
|
+
}
|
|
12209
|
+
throw err7;
|
|
12210
|
+
}
|
|
12211
|
+
try {
|
|
12212
|
+
const result2 = await runUiAgent(
|
|
12213
|
+
prompt,
|
|
12214
|
+
"Carrier CLI browsing agent",
|
|
12215
|
+
runtime.env
|
|
12216
|
+
);
|
|
12217
|
+
runtime.dispatchedTaskIds.add(result2.task_id);
|
|
12218
|
+
ctx.audit({
|
|
12219
|
+
tool_name: toolName,
|
|
12220
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12221
|
+
status: result2.status === "error" ? "error" : "ok",
|
|
12222
|
+
dry_run: false,
|
|
12223
|
+
duration_ms: Date.now() - start,
|
|
12224
|
+
event_type: "ui_agent_dispatch"
|
|
12225
|
+
});
|
|
12226
|
+
return {
|
|
12227
|
+
...result2.status === "error" ? { isError: true } : {},
|
|
12228
|
+
content: [
|
|
12229
|
+
{
|
|
12230
|
+
type: "text",
|
|
12231
|
+
text: JSON.stringify(
|
|
12232
|
+
{
|
|
12233
|
+
status: result2.status,
|
|
12234
|
+
task_id: result2.task_id,
|
|
12235
|
+
...result2.session_viewer_url ? { session_viewer_url: result2.session_viewer_url } : {},
|
|
12236
|
+
result: result2.result,
|
|
12237
|
+
session_spend_usd: runtime.spend.costUsd,
|
|
12238
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
12239
|
+
note: "The run completed in-process. Task records live only for this CLI session \u2014 ui_agent_status can read this task_id until the process exits."
|
|
12240
|
+
},
|
|
12241
|
+
null,
|
|
12242
|
+
2
|
|
12243
|
+
)
|
|
12244
|
+
}
|
|
12245
|
+
]
|
|
12246
|
+
};
|
|
12247
|
+
} catch (err7) {
|
|
12248
|
+
ctx.audit({
|
|
12249
|
+
tool_name: toolName,
|
|
12250
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12251
|
+
status: "error",
|
|
12252
|
+
dry_run: false,
|
|
12253
|
+
duration_ms: Date.now() - start,
|
|
12254
|
+
event_type: "ui_agent_dispatch"
|
|
12255
|
+
});
|
|
12256
|
+
return {
|
|
12257
|
+
isError: true,
|
|
12258
|
+
content: [
|
|
12259
|
+
{
|
|
12260
|
+
type: "text",
|
|
12261
|
+
text: JSON.stringify({
|
|
12262
|
+
error: "ui_agent_failed",
|
|
12263
|
+
message: err7 instanceof Error ? err7.message : String(err7)
|
|
12264
|
+
})
|
|
12265
|
+
}
|
|
12266
|
+
]
|
|
12267
|
+
};
|
|
12268
|
+
}
|
|
12269
|
+
}
|
|
12144
12270
|
);
|
|
12145
12271
|
server2.registerTool(
|
|
12146
12272
|
"ui_agent_status",
|
|
12147
12273
|
{
|
|
12148
|
-
title: "
|
|
12149
|
-
description: "
|
|
12274
|
+
title: "Poll Browsing Agent Task Status",
|
|
12275
|
+
description: "Returns the current state of a browsing-agent task by `task_id`, including its result once finished. In the local CLI, ui_agent_ask already returns the finished result, so this is mainly for re-reading a task within the same session. Task records do not survive the CLI process exiting. Requires `read` scope.",
|
|
12150
12276
|
inputSchema: {
|
|
12151
|
-
task_id: z22.string().describe("
|
|
12277
|
+
task_id: z22.string().describe("Task ID returned by ui_agent_ask")
|
|
12152
12278
|
}
|
|
12153
12279
|
},
|
|
12154
|
-
async (
|
|
12280
|
+
async (args) => {
|
|
12281
|
+
const toolName = "ui_agent_status";
|
|
12282
|
+
if (ctx.props.scope.length === 0) {
|
|
12283
|
+
return scopeError2(toolName, "read", ctx.props.scope);
|
|
12284
|
+
}
|
|
12285
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
12286
|
+
return notConfiguredError(toolName);
|
|
12287
|
+
}
|
|
12288
|
+
const task = await getUiAgentTask(
|
|
12289
|
+
args.task_id,
|
|
12290
|
+
runtime.env
|
|
12291
|
+
);
|
|
12292
|
+
if (!task) {
|
|
12293
|
+
return {
|
|
12294
|
+
isError: true,
|
|
12295
|
+
content: [
|
|
12296
|
+
{
|
|
12297
|
+
type: "text",
|
|
12298
|
+
text: JSON.stringify({
|
|
12299
|
+
error: "task_not_found",
|
|
12300
|
+
message: `No task record for task_id=${args.task_id}. In the local CLI these live in memory for the lifetime of the process, so a task from an earlier session is gone rather than expired.`
|
|
12301
|
+
})
|
|
12302
|
+
}
|
|
12303
|
+
]
|
|
12304
|
+
};
|
|
12305
|
+
}
|
|
12306
|
+
return {
|
|
12307
|
+
content: [
|
|
12308
|
+
{
|
|
12309
|
+
type: "text",
|
|
12310
|
+
text: JSON.stringify(task, null, 2)
|
|
12311
|
+
}
|
|
12312
|
+
]
|
|
12313
|
+
};
|
|
12314
|
+
}
|
|
12155
12315
|
);
|
|
12156
12316
|
}
|
|
12157
12317
|
|
|
@@ -12177,8 +12337,15 @@ var stdioEnv = {
|
|
|
12177
12337
|
ASSETS: null,
|
|
12178
12338
|
CARRIER_USERS: null,
|
|
12179
12339
|
DOWNLOADS: null,
|
|
12180
|
-
MANUS_API_KEY: "",
|
|
12181
12340
|
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY ?? "",
|
|
12341
|
+
// Browser Use Cloud. Read from the environment with a real fallback, the way
|
|
12342
|
+
// STRIPE_SECRET_KEY above and the wallet values below are — unlike the
|
|
12343
|
+
// MANUS_API_KEY this replaces, which was hardcoded to "" with no fallback at
|
|
12344
|
+
// all, so every ui_agent_* tool returned a configuration error to every
|
|
12345
|
+
// caller from the day it shipped. Absent is still a valid state: the tools
|
|
12346
|
+
// stay registered and say so. Runs are billed to this key at ~$0.20-1.00
|
|
12347
|
+
// each, so dispatch is capped per process — see ui-agent-runtime.ts.
|
|
12348
|
+
BROWSER_USE_API_KEY: process.env.BROWSER_USE_API_KEY,
|
|
12182
12349
|
// Prepaid-wallet configuration. Read from the environment so a local operator
|
|
12183
12350
|
// who holds these values gets working wallet tools; when absent the tools stay
|
|
12184
12351
|
// registered (catalog parity with the Worker) and return an explicit
|
|
@@ -12222,15 +12389,22 @@ registerCountryHistoryTool(server, toolCtx);
|
|
|
12222
12389
|
registerDepletionEventsTool(server, toolCtx);
|
|
12223
12390
|
registerAllApps(server, toolCtx);
|
|
12224
12391
|
registerAllPricingTools(server, toolCtx);
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12392
|
+
var uiAgentRuntime = createUiAgentRuntime();
|
|
12393
|
+
var portalOptIn = readPortalOptIn();
|
|
12394
|
+
warnPortalOptIn(portalOptIn);
|
|
12395
|
+
registerScheduleAndUsageTools(server, toolCtx, uiAgentRuntime);
|
|
12396
|
+
registerAllUiAgentTools(server, toolCtx, uiAgentRuntime, portalOptIn);
|
|
12397
|
+
registerUiAgentAskTools(server, toolCtx, uiAgentRuntime);
|
|
12228
12398
|
registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
12229
12399
|
registerWalletTools(server, { env: stdioEnv, props });
|
|
12230
12400
|
registerGreenzoneTools(server, toolCtx);
|
|
12231
|
-
registerUiAgentGenericTools(server, toolCtx);
|
|
12232
|
-
registerStorefrontLogoTools(
|
|
12233
|
-
|
|
12401
|
+
registerUiAgentGenericTools(server, toolCtx, uiAgentRuntime);
|
|
12402
|
+
registerStorefrontLogoTools(
|
|
12403
|
+
server,
|
|
12404
|
+
stdioEnv,
|
|
12405
|
+
props
|
|
12406
|
+
);
|
|
12407
|
+
registerStorefrontDeployTools(server, props);
|
|
12234
12408
|
registerAllPrompts(server);
|
|
12235
12409
|
var transport = new StdioServerTransport();
|
|
12236
12410
|
await server.connect(transport);
|