@carrierllc/mcp 0.9.3 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/{chunk-KHNJNJX3.js → chunk-DP4ICKYF.js} +668 -8
- package/dist/chunk-DP4ICKYF.js.map +1 -0
- package/dist/cli.js +40 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1524 -1303
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/marketplace.json +2 -2
- package/plugin/carrier/.claude-plugin/plugin.json +1 -1
- package/dist/chunk-KHNJNJX3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CARRIER_VERSION,
|
|
4
4
|
OCS_MAX_USAGE_WINDOW_DAYS,
|
|
5
|
+
OcsApiError,
|
|
6
|
+
OcsClient,
|
|
5
7
|
ROUTER_RULES,
|
|
6
8
|
TARGET_IDS,
|
|
7
|
-
|
|
8
|
-
applyParamRenames,
|
|
9
|
+
activePeriodFromPackages,
|
|
9
10
|
applySubscriberFilters,
|
|
10
11
|
buildListSubscriberParams,
|
|
11
12
|
buildRouterCatalog,
|
|
@@ -33,21 +34,24 @@ import {
|
|
|
33
34
|
networkEventsOverPeriodParams,
|
|
34
35
|
normalizePackageTemplate,
|
|
35
36
|
normalizePackageTemplateChanges,
|
|
37
|
+
ocsLocalDateTime,
|
|
38
|
+
packageActivePeriodParams,
|
|
39
|
+
prepaidPackageLimits,
|
|
36
40
|
probeAll,
|
|
37
41
|
provisionClerk,
|
|
38
42
|
rankTargets,
|
|
39
43
|
recordToolDescription,
|
|
44
|
+
recurringIdForPackage,
|
|
40
45
|
recurringPackageParams,
|
|
41
46
|
renderHtml,
|
|
42
47
|
repairPlanFor,
|
|
43
|
-
runWithBudget,
|
|
44
48
|
storefrontClerkUrls,
|
|
45
49
|
subscriberIdParams,
|
|
46
50
|
subscriberRows,
|
|
47
51
|
usageOverPeriodParams,
|
|
48
52
|
verifyStorefront,
|
|
49
53
|
withOcsListSummary
|
|
50
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-DP4ICKYF.js";
|
|
51
55
|
import "./chunk-SHKKVIIA.js";
|
|
52
56
|
|
|
53
57
|
// src/index.ts
|
|
@@ -58,59 +62,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
58
62
|
import { z } from "zod";
|
|
59
63
|
import * as Sentry from "@sentry/cloudflare";
|
|
60
64
|
|
|
61
|
-
// src/client.ts
|
|
62
|
-
var OcsApiError = class extends Error {
|
|
63
|
-
constructor(code, message, method) {
|
|
64
|
-
super(`[${method}] OCS error ${code}: ${message}`);
|
|
65
|
-
this.code = code;
|
|
66
|
-
this.method = method;
|
|
67
|
-
this.name = "OcsApiError";
|
|
68
|
-
}
|
|
69
|
-
code;
|
|
70
|
-
method;
|
|
71
|
-
};
|
|
72
|
-
var OcsClient = class {
|
|
73
|
-
baseUrl;
|
|
74
|
-
token;
|
|
75
|
-
constructor(baseUrl2, token2) {
|
|
76
|
-
this.baseUrl = baseUrl2.replace(/\/+$/, "");
|
|
77
|
-
this.token = token2;
|
|
78
|
-
}
|
|
79
|
-
async call(method, params = {}) {
|
|
80
|
-
await acquireEndpointSlot(this.token, method, "interactive");
|
|
81
|
-
const url = `${this.baseUrl}/v1?token=${this.token}`;
|
|
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
|
-
}
|
|
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);
|
|
96
|
-
}
|
|
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
|
-
});
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
65
|
// ../../packages/carrier-ai/dist/index.js
|
|
115
66
|
import { AwsClient } from "aws4fetch";
|
|
116
67
|
var AI_TIER_POLICY = {
|
|
@@ -360,6 +311,469 @@ async function invokeModelRaw(creds, payload, opts = {}, signal) {
|
|
|
360
311
|
signal?.removeEventListener("abort", onAbort);
|
|
361
312
|
}
|
|
362
313
|
}
|
|
314
|
+
var PENDING_ASK_PREFIX = "manus_pending_ask:";
|
|
315
|
+
var PENDING_ASK_TTL_SECONDS = 24 * 3600;
|
|
316
|
+
var OCS_DASHBOARD_DEFAULT = "https://ocs.esimvault.cloud";
|
|
317
|
+
function buildLoginPreamble(dashboardUrl) {
|
|
318
|
+
return `STEP 1 \u2014 LOGIN:
|
|
319
|
+
Navigate to ${dashboardUrl}/login (or the main page if no /login path).
|
|
320
|
+
Enter the OCS portal username and password provided below.
|
|
321
|
+
Wait for the dashboard to fully load after login.
|
|
322
|
+
If already logged in (session cookie persists), skip to STEP 2.
|
|
323
|
+
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
function assembleUiAgentPrompt(operationPrompt, credentials, dashboardUrl) {
|
|
327
|
+
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.
|
|
328
|
+
|
|
329
|
+
OCS PORTAL CREDENTIALS (use these to log in \u2014 NEVER include them in your output):
|
|
330
|
+
Username: ${credentials.username}
|
|
331
|
+
Password: ${credentials.password}
|
|
332
|
+
|
|
333
|
+
` + buildLoginPreamble(dashboardUrl) + `STEP 2 \u2014 OPERATION:
|
|
334
|
+
` + operationPrompt + `
|
|
335
|
+
|
|
336
|
+
STEP 3 \u2014 VERIFICATION:
|
|
337
|
+
After completing the operation, verify the result by checking the dashboard shows the expected state.
|
|
338
|
+
Take a screenshot of the final state for audit purposes.
|
|
339
|
+
Report success or failure with a clear summary.`;
|
|
340
|
+
}
|
|
341
|
+
function redactCredentials(prompt, credentials) {
|
|
342
|
+
return prompt.replaceAll(credentials.password, "***REDACTED***").replaceAll(credentials.username, "***REDACTED***");
|
|
343
|
+
}
|
|
344
|
+
function buildCreateSteeringListPrompt(params, dashboardUrl) {
|
|
345
|
+
return `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
346
|
+
Create a new steering list with the following details:
|
|
347
|
+
Name: ${params.name}
|
|
348
|
+
` + (params.description ? ` Description: ${params.description}
|
|
349
|
+
` : "") + `Click the "Create" or "Add" button to create the steering list.
|
|
350
|
+
After creation, note the new steering list ID from the dashboard.
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
function buildBuildSteeringListPrompt(params, dashboardUrl) {
|
|
354
|
+
const add = params.add_operators ?? [];
|
|
355
|
+
const remove = params.remove_operators ?? [];
|
|
356
|
+
return `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
357
|
+
Open steering list ID ${params.steering_list_id} for editing.
|
|
358
|
+
Operator type: ${params.operator_type ?? "priority"}
|
|
359
|
+
` + (add.length > 0 ? `Add the following operators: ${add.join(", ")}
|
|
360
|
+
` : "") + (remove.length > 0 ? `Remove the following operators: ${remove.join(", ")}
|
|
361
|
+
` : "") + `Save the changes and verify the updated operator list.
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
function buildSetAccountSteeringListPrompt(params, dashboardUrl) {
|
|
365
|
+
return `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
|
|
366
|
+
Open account ID ${params.account_id}.
|
|
367
|
+
` + (params.steering_list_id === 0 ? `Remove/unset the steering list assignment from this account.
|
|
368
|
+
` : `Assign steering list ID ${params.steering_list_id} to this account.
|
|
369
|
+
`) + `Save the changes and verify the steering list assignment is updated.
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
function buildCreateAccountPrompt(params, dashboardUrl) {
|
|
373
|
+
return `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
|
|
374
|
+
Create a new account with the following details:
|
|
375
|
+
Name: ${params.name}
|
|
376
|
+
` + (params.description ? ` Description: ${params.description}
|
|
377
|
+
` : "") + (params.initial_balance ? ` Initial balance: ${params.initial_balance}
|
|
378
|
+
` : "") + `Click the "Create" or "Add" button.
|
|
379
|
+
After creation, note the new account ID from the dashboard.
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
function buildCreateDestinationListPrompt(params, dashboardUrl) {
|
|
383
|
+
const prefixes = params.prefixes ?? [];
|
|
384
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
385
|
+
Create a new destination list with the following details:
|
|
386
|
+
Name: ${params.name}
|
|
387
|
+
` + (params.description ? ` Description: ${params.description}
|
|
388
|
+
` : "") + (prefixes.length > 0 ? ` Prefixes to add: ${prefixes.join(", ")}
|
|
389
|
+
` : "") + `Save the new destination list and note the ID.
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
function buildEditDestinationListPrompt(params, dashboardUrl) {
|
|
393
|
+
const add = params.add_prefixes ?? [];
|
|
394
|
+
const remove = params.remove_prefixes ?? [];
|
|
395
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
396
|
+
Open destination list ID ${params.destination_list_id} for editing.
|
|
397
|
+
` + (params.new_name ? `Rename to: ${params.new_name}
|
|
398
|
+
` : "") + (add.length > 0 ? `Add prefixes: ${add.join(", ")}
|
|
399
|
+
` : "") + (remove.length > 0 ? `Remove prefixes: ${remove.join(", ")}
|
|
400
|
+
` : "") + `Save the changes and verify the updated prefix list.
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
function buildDeleteDestinationListPrompt(params, dashboardUrl) {
|
|
404
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
405
|
+
Find destination list ID ${params.destination_list_id}.
|
|
406
|
+
Delete this destination list. Confirm the deletion when prompted.
|
|
407
|
+
Verify the list no longer appears in the dashboard.
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
function buildDeletePackageTemplatePrompt(params, dashboardUrl) {
|
|
411
|
+
return `Navigate to the Package Templates section of the OCS dashboard at ${dashboardUrl}.
|
|
412
|
+
Find package template ID ${params.template_id}.
|
|
413
|
+
Delete this package template. Confirm the deletion when prompted.
|
|
414
|
+
Verify the template no longer appears in the template list.
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
function buildEditLocationZonePrompt(params, dashboardUrl) {
|
|
418
|
+
const add = params.add_countries ?? [];
|
|
419
|
+
const remove = params.remove_countries ?? [];
|
|
420
|
+
return `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
|
|
421
|
+
Open location zone ID ${params.zone_id} for editing.
|
|
422
|
+
` + (params.new_name ? `Rename to: ${params.new_name}
|
|
423
|
+
` : "") + (add.length > 0 ? `Add countries: ${add.join(", ")}
|
|
424
|
+
` : "") + (remove.length > 0 ? `Remove countries: ${remove.join(", ")}
|
|
425
|
+
` : "") + `Save the changes and verify the updated country list.
|
|
426
|
+
`;
|
|
427
|
+
}
|
|
428
|
+
function buildDeleteLocationZonePrompt(params, dashboardUrl) {
|
|
429
|
+
return `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
|
|
430
|
+
Find location zone ID ${params.zone_id}.
|
|
431
|
+
Delete this location zone. Confirm the deletion when prompted.
|
|
432
|
+
If the dashboard shows an error (e.g. zone in use by active templates), report the error.
|
|
433
|
+
Verify the zone no longer appears in the zone list.
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
var BROWSER_USE_API = "https://api.browser-use.com/api/v4";
|
|
437
|
+
var RUN_TIMEOUT_MS = 10 * 6e4;
|
|
438
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
439
|
+
function browserUseConfigured(env) {
|
|
440
|
+
return Boolean(env.apiKey);
|
|
441
|
+
}
|
|
442
|
+
function apiKey(env) {
|
|
443
|
+
const key = env.apiKey;
|
|
444
|
+
if (!key) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
"BROWSER_USE_API_KEY is not set \u2014 the browser-use provider cannot run."
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return key;
|
|
450
|
+
}
|
|
451
|
+
async function v4(env, path, init) {
|
|
452
|
+
const res = await fetch(`${BROWSER_USE_API}${path}`, {
|
|
453
|
+
method: init?.method ?? "GET",
|
|
454
|
+
headers: {
|
|
455
|
+
"X-Browser-Use-API-Key": apiKey(env),
|
|
456
|
+
"Content-Type": "application/json"
|
|
457
|
+
},
|
|
458
|
+
...init?.body ? { body: JSON.stringify(init.body) } : {}
|
|
459
|
+
});
|
|
460
|
+
let data = null;
|
|
461
|
+
try {
|
|
462
|
+
data = await res.json();
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
return { ok: res.ok, status: res.status, data };
|
|
466
|
+
}
|
|
467
|
+
function mapStatus(raw) {
|
|
468
|
+
switch ((raw ?? "").toLowerCase()) {
|
|
469
|
+
case "finished":
|
|
470
|
+
case "completed":
|
|
471
|
+
case "success":
|
|
472
|
+
case "stopped":
|
|
473
|
+
return "stopped";
|
|
474
|
+
case "failed":
|
|
475
|
+
case "error":
|
|
476
|
+
return "error";
|
|
477
|
+
case "paused":
|
|
478
|
+
case "waiting":
|
|
479
|
+
case "needs_input":
|
|
480
|
+
return "waiting";
|
|
481
|
+
default:
|
|
482
|
+
return "running";
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function defaultRedact(text) {
|
|
486
|
+
return text.replace(/(\bpassword\b\s*[:=]\s*)\S+/gi, "$1***REDACTED***").replace(/(\busername\b\s*[:=]\s*)\S+/gi, "$1***REDACTED***");
|
|
487
|
+
}
|
|
488
|
+
function toTaskResult(run, redact) {
|
|
489
|
+
const status = mapStatus(run.status);
|
|
490
|
+
const scrub = (text) => defaultRedact(redact ? redact(text) : text);
|
|
491
|
+
const payload = run.result ?? run.output;
|
|
492
|
+
if (payload && typeof payload === "object") {
|
|
493
|
+
const p = payload;
|
|
494
|
+
return {
|
|
495
|
+
success: status === "stopped" && !run.error,
|
|
496
|
+
summary: scrub(
|
|
497
|
+
typeof p.summary === "string" ? p.summary : JSON.stringify(payload)
|
|
498
|
+
).slice(0, 2e3),
|
|
499
|
+
...typeof p.entity_id === "string" ? { entity_id: p.entity_id } : {},
|
|
500
|
+
...run.error ? { error_message: scrub(run.error) } : {},
|
|
501
|
+
// The hosted agent does not report a screenshot count. Zero is honest;
|
|
502
|
+
// inventing one would put a fabricated number in an audit row.
|
|
503
|
+
screenshots_taken: 0
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
success: status === "stopped" && !run.error,
|
|
508
|
+
summary: typeof payload === "string" ? scrub(payload).slice(0, 2e3) : "",
|
|
509
|
+
...run.error ? { error_message: scrub(run.error) } : {},
|
|
510
|
+
screenshots_taken: 0
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function viewerUrl(run) {
|
|
514
|
+
return run.sessionId ? `https://cloud.browser-use.com/sessions/${run.sessionId}` : void 0;
|
|
515
|
+
}
|
|
516
|
+
function runSpend(run) {
|
|
517
|
+
if (!run || typeof run.totalCostUsd !== "number") return null;
|
|
518
|
+
return {
|
|
519
|
+
costUsd: run.totalCostUsd,
|
|
520
|
+
inputTokens: run.totalInputTokens ?? 0,
|
|
521
|
+
outputTokens: run.totalOutputTokens ?? 0
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async function createRun(env, task, opts) {
|
|
525
|
+
const body2 = { task };
|
|
526
|
+
if (opts?.model) body2.model = opts.model;
|
|
527
|
+
if (opts?.outputSchema) body2.schema = opts.outputSchema;
|
|
528
|
+
const res = await v4(env, "/runs", { method: "POST", body: body2 });
|
|
529
|
+
if (!res.ok) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Browser Use run create failed: HTTP ${res.status} ${JSON.stringify(res.data).slice(0, 300)}`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const run = res.data;
|
|
535
|
+
if (!run?.id) throw new Error("Browser Use run create returned no id");
|
|
536
|
+
const url = viewerUrl(run);
|
|
537
|
+
return { runId: run.id, ...url ? { sessionViewerUrl: url } : {} };
|
|
538
|
+
}
|
|
539
|
+
async function getRun(env, runId) {
|
|
540
|
+
const res = await v4(env, `/runs/${encodeURIComponent(runId)}`);
|
|
541
|
+
if (!res.ok) return null;
|
|
542
|
+
return res.data;
|
|
543
|
+
}
|
|
544
|
+
async function waitForRun(env, runId, timeoutMs = RUN_TIMEOUT_MS) {
|
|
545
|
+
const deadline = Date.now() + timeoutMs;
|
|
546
|
+
let last = null;
|
|
547
|
+
while (Date.now() < deadline) {
|
|
548
|
+
last = await getRun(env, runId);
|
|
549
|
+
if (last && mapStatus(last.status) !== "running") return last;
|
|
550
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
551
|
+
}
|
|
552
|
+
return last;
|
|
553
|
+
}
|
|
554
|
+
function toSteelTask(taskId, run, opts) {
|
|
555
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
556
|
+
const status = mapStatus(run?.status);
|
|
557
|
+
const url = run ? viewerUrl(run) : void 0;
|
|
558
|
+
return {
|
|
559
|
+
task_id: taskId,
|
|
560
|
+
status,
|
|
561
|
+
...opts?.ownerSub ? { owner_sub: opts.ownerSub } : {},
|
|
562
|
+
// The console's task detail view renders steel_session_id. It is the
|
|
563
|
+
// provider's session id, whoever the provider is.
|
|
564
|
+
...run?.sessionId ? { steel_session_id: run.sessionId } : {},
|
|
565
|
+
...run?.id ? { run_id: run.id } : {},
|
|
566
|
+
...url ? { session_viewer_url: url } : {},
|
|
567
|
+
...opts?.title ? { title: opts.title } : {},
|
|
568
|
+
created_at: opts?.createdAt ?? now,
|
|
569
|
+
updated_at: now,
|
|
570
|
+
...run && status !== "running" ? { result: toTaskResult(run, opts?.redact) } : {},
|
|
571
|
+
// Scrubbed on the same grounds as the result: the question is agent-authored
|
|
572
|
+
// text, and an agent that pauses to ask for help is exactly the one likely
|
|
573
|
+
// to quote its own instructions — password line included — while explaining
|
|
574
|
+
// what it is stuck on. This string is stored in KV and handed to operators
|
|
575
|
+
// by ui_agent_list_pending.
|
|
576
|
+
...status === "waiting" ? { pending_question: pendingQuestion(run, opts?.redact) } : {}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function pendingQuestion(run, redact) {
|
|
580
|
+
const payload = run?.result;
|
|
581
|
+
const q = payload && typeof payload === "object" ? payload.question : void 0;
|
|
582
|
+
if (typeof q !== "string" || q.length === 0) {
|
|
583
|
+
return "The browser agent is paused and needs input to continue.";
|
|
584
|
+
}
|
|
585
|
+
return defaultRedact(redact ? redact(q) : q);
|
|
586
|
+
}
|
|
587
|
+
function taskKey(taskId) {
|
|
588
|
+
return `steel_task:${taskId}`;
|
|
589
|
+
}
|
|
590
|
+
var TASK_TTL_SECONDS = 7 * 24 * 3600;
|
|
591
|
+
async function beginUiAgentRun(prompt, title, env, opts) {
|
|
592
|
+
if (!browserUseConfigured(env)) {
|
|
593
|
+
throw new Error("BROWSER_USE_API_KEY is not configured");
|
|
594
|
+
}
|
|
595
|
+
const taskId = opts?.taskId ?? `bu_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
596
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
597
|
+
const base = {
|
|
598
|
+
task_id: taskId,
|
|
599
|
+
status: "running",
|
|
600
|
+
title,
|
|
601
|
+
created_at: createdAt,
|
|
602
|
+
updated_at: createdAt,
|
|
603
|
+
...opts?.ownerSub ? { owner_sub: opts.ownerSub } : {}
|
|
604
|
+
};
|
|
605
|
+
await putTask(env, base);
|
|
606
|
+
let created;
|
|
607
|
+
try {
|
|
608
|
+
created = await createRun(env, prompt, {
|
|
609
|
+
...opts?.model ? { model: opts.model } : {},
|
|
610
|
+
...opts?.outputSchema ? { outputSchema: opts.outputSchema } : {}
|
|
611
|
+
});
|
|
612
|
+
} catch (err7) {
|
|
613
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
614
|
+
await putTask(env, {
|
|
615
|
+
...base,
|
|
616
|
+
status: "error",
|
|
617
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
618
|
+
result: {
|
|
619
|
+
success: false,
|
|
620
|
+
summary: `Failed to start a Browser Use run: ${message}`,
|
|
621
|
+
error_message: message,
|
|
622
|
+
screenshots_taken: 0
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
throw err7;
|
|
626
|
+
}
|
|
627
|
+
const dispatched = {
|
|
628
|
+
...base,
|
|
629
|
+
run_id: created.runId,
|
|
630
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
631
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
632
|
+
};
|
|
633
|
+
await putTask(env, dispatched);
|
|
634
|
+
const complete = async () => {
|
|
635
|
+
let run;
|
|
636
|
+
try {
|
|
637
|
+
run = await waitForRun(env, created.runId);
|
|
638
|
+
} catch (err7) {
|
|
639
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
640
|
+
const failed = {
|
|
641
|
+
success: false,
|
|
642
|
+
summary: `Browser Use run failed: ${message}`,
|
|
643
|
+
error_message: message,
|
|
644
|
+
screenshots_taken: 0
|
|
645
|
+
};
|
|
646
|
+
await putTask(env, {
|
|
647
|
+
...dispatched,
|
|
648
|
+
status: "error",
|
|
649
|
+
result: failed,
|
|
650
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
651
|
+
});
|
|
652
|
+
writeAgentAudit(env, {
|
|
653
|
+
event_type: "task_stopped",
|
|
654
|
+
stop_reason: "error",
|
|
655
|
+
task_id: taskId
|
|
656
|
+
});
|
|
657
|
+
return {
|
|
658
|
+
task_id: taskId,
|
|
659
|
+
status: "error",
|
|
660
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
661
|
+
result: failed
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const final = toSteelTask(taskId, run, {
|
|
665
|
+
title,
|
|
666
|
+
...opts?.ownerSub ? { ownerSub: opts.ownerSub } : {},
|
|
667
|
+
...opts?.redact ? { redact: opts.redact } : {},
|
|
668
|
+
createdAt
|
|
669
|
+
});
|
|
670
|
+
if (!final.session_viewer_url && created.sessionViewerUrl) {
|
|
671
|
+
final.session_viewer_url = created.sessionViewerUrl;
|
|
672
|
+
}
|
|
673
|
+
final.run_id = created.runId;
|
|
674
|
+
await putTask(env, final);
|
|
675
|
+
if (final.status === "waiting") {
|
|
676
|
+
await writePendingAsk(env, final);
|
|
677
|
+
}
|
|
678
|
+
const spend = runSpend(run);
|
|
679
|
+
writeAgentAudit(env, {
|
|
680
|
+
event_type: "task_stopped",
|
|
681
|
+
stop_reason: final.status === "waiting" ? "ask" : final.status === "stopped" ? "finish" : final.status === "running" ? "timeout" : "error",
|
|
682
|
+
task_id: taskId,
|
|
683
|
+
...spend ? { spend } : {}
|
|
684
|
+
});
|
|
685
|
+
return {
|
|
686
|
+
task_id: taskId,
|
|
687
|
+
status: final.status,
|
|
688
|
+
...final.session_viewer_url ? { session_viewer_url: final.session_viewer_url } : {},
|
|
689
|
+
...final.result ? { result: final.result } : {},
|
|
690
|
+
...spend ? { spend } : {}
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
return {
|
|
694
|
+
task_id: taskId,
|
|
695
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
696
|
+
complete
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function runUiAgent(prompt, title, env, opts) {
|
|
700
|
+
const handle = await beginUiAgentRun(prompt, title, env, opts);
|
|
701
|
+
return await handle.complete();
|
|
702
|
+
}
|
|
703
|
+
async function getUiAgentTask(taskId, env) {
|
|
704
|
+
const raw = await env.store.get(taskKey(taskId));
|
|
705
|
+
if (!raw) return null;
|
|
706
|
+
try {
|
|
707
|
+
return JSON.parse(raw);
|
|
708
|
+
} catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function resumeUiAgentTask(taskId, reply, env) {
|
|
713
|
+
const pendingRaw = await env.store.get(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
714
|
+
if (!pendingRaw) {
|
|
715
|
+
return { ok: false, error: "No pending-ask entry found for this task_id." };
|
|
716
|
+
}
|
|
717
|
+
const task = await getUiAgentTask(taskId, env);
|
|
718
|
+
if (!task) {
|
|
719
|
+
return { ok: false, error: "Task record not found." };
|
|
720
|
+
}
|
|
721
|
+
await env.store.delete(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
722
|
+
const { pending_question: _dropped, ...rest } = task;
|
|
723
|
+
await putTask(env, {
|
|
724
|
+
...rest,
|
|
725
|
+
status: "running",
|
|
726
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
727
|
+
});
|
|
728
|
+
writeAgentAudit(env, {
|
|
729
|
+
event_type: "task_stopped",
|
|
730
|
+
stop_reason: "resumed",
|
|
731
|
+
task_id: taskId,
|
|
732
|
+
replyLen: reply.length
|
|
733
|
+
});
|
|
734
|
+
return { ok: true };
|
|
735
|
+
}
|
|
736
|
+
async function putTask(env, task) {
|
|
737
|
+
await env.store.put(taskKey(task.task_id), JSON.stringify(task), {
|
|
738
|
+
expirationTtl: TASK_TTL_SECONDS
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async function writePendingAsk(env, task) {
|
|
742
|
+
const entry = {
|
|
743
|
+
task_id: task.task_id,
|
|
744
|
+
question: task.pending_question ?? "The browser agent is paused and needs input to continue.",
|
|
745
|
+
task_url: task.session_viewer_url ?? "",
|
|
746
|
+
asked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
747
|
+
};
|
|
748
|
+
await env.store.put(
|
|
749
|
+
`${PENDING_ASK_PREFIX}${task.task_id}`,
|
|
750
|
+
JSON.stringify(entry),
|
|
751
|
+
{ expirationTtl: PENDING_ASK_TTL_SECONDS }
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
function writeAgentAudit(env, entry) {
|
|
755
|
+
try {
|
|
756
|
+
env.audit({
|
|
757
|
+
blobs: [
|
|
758
|
+
"steel_agent",
|
|
759
|
+
entry.event_type,
|
|
760
|
+
entry.stop_reason,
|
|
761
|
+
entry.task_id,
|
|
762
|
+
"0",
|
|
763
|
+
"browser_use"
|
|
764
|
+
],
|
|
765
|
+
doubles: [
|
|
766
|
+
entry.replyLen ?? 0,
|
|
767
|
+
entry.spend?.costUsd ?? 0,
|
|
768
|
+
entry.spend?.inputTokens ?? 0,
|
|
769
|
+
entry.spend?.outputTokens ?? 0
|
|
770
|
+
],
|
|
771
|
+
indexes: [entry.task_id]
|
|
772
|
+
});
|
|
773
|
+
} catch (err7) {
|
|
774
|
+
console.error(`[browser-use-agent] audit write failed: ${err7.message}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
363
777
|
|
|
364
778
|
// src/billing.ts
|
|
365
779
|
var TIER_CALL_LIMITS = {
|
|
@@ -907,7 +1321,12 @@ function registerAllTools(server2, ctx) {
|
|
|
907
1321
|
TOOL_SCOPES["modify_account_balance"],
|
|
908
1322
|
ctx,
|
|
909
1323
|
async ({ accountId, amount, mode }, token2) => {
|
|
910
|
-
const params = { accountId
|
|
1324
|
+
const params = { accountId };
|
|
1325
|
+
if (mode === "set") {
|
|
1326
|
+
params.setBalance = amount;
|
|
1327
|
+
} else {
|
|
1328
|
+
params.amount = amount;
|
|
1329
|
+
}
|
|
911
1330
|
return ocsCall(ctx.env, token2, "modifyAccountBalance", params);
|
|
912
1331
|
}
|
|
913
1332
|
)
|
|
@@ -1288,11 +1707,13 @@ function registerAllTools(server2, ctx) {
|
|
|
1288
1707
|
"move_subscriber_range_to_account",
|
|
1289
1708
|
{
|
|
1290
1709
|
title: "Move Subscribers to Account",
|
|
1291
|
-
description: "Use this to move a contiguous
|
|
1710
|
+
description: "BREAKING CHANGE (2026-08-28): this tool's contract was rebuilt around account-ID ranges to match what OCS actually accepts \u2014 ICCID-pair targeting (`iccidFrom`/`iccidTo` + a single `accountId`) is gone. Zero field names were ever shared with OCS's real `moveSubscriberRangeToAccount` shape (verified live 2026-08-24 \u2014 see packages/ocs-spec/ocs-accepted-params.json), so every prior call to this tool failed outright; nothing that used to work stops working. Use this to move a contiguous range of subscribers, identified by IMSI or ICCID, from one account to another. Cross-reseller moves lose the subscribers' packages. Params: `srcAccountId` (source account ID from `list_reseller_accounts`), `destAccount` (destination account ID), `rangeType` ('IMSI' | 'ICCID' \u2014 which identifier `rangeStart`/`rangeEnd` are expressed in), `rangeStart` (inclusive start of the range), `rangeEnd` (inclusive end of the range). Returns: OCS confirmation with the count of subscribers moved. Do NOT use this for a single subscriber move \u2014 provide identical rangeStart and rangeEnd. Always call `list_subscribers` on the range first to verify the correct subscribers are included.",
|
|
1292
1711
|
inputSchema: {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1712
|
+
srcAccountId: z.number().describe("Source account ID to move subscribers FROM"),
|
|
1713
|
+
destAccount: z.number().describe("Destination account ID to move subscribers TO"),
|
|
1714
|
+
rangeType: z.enum(["IMSI", "ICCID"]).describe("Whether rangeStart/rangeEnd identify subscribers by IMSI or ICCID"),
|
|
1715
|
+
rangeStart: z.string().describe("Inclusive start of the identifier range"),
|
|
1716
|
+
rangeEnd: z.string().describe("Inclusive end of the identifier range"),
|
|
1296
1717
|
...DRY_RUN_FIELD
|
|
1297
1718
|
},
|
|
1298
1719
|
annotations: { destructiveHint: true }
|
|
@@ -1302,10 +1723,12 @@ function registerAllTools(server2, ctx) {
|
|
|
1302
1723
|
"moveSubscriberRangeToAccount",
|
|
1303
1724
|
TOOL_SCOPES["move_subscriber_range_to_account"],
|
|
1304
1725
|
ctx,
|
|
1305
|
-
async ({
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1726
|
+
async ({ srcAccountId, destAccount, rangeType, rangeStart, rangeEnd }, token2) => ocsCall(ctx.env, token2, "moveSubscriberRangeToAccount", {
|
|
1727
|
+
srcAccountId,
|
|
1728
|
+
destAccount,
|
|
1729
|
+
rangeType,
|
|
1730
|
+
rangeStart,
|
|
1731
|
+
rangeEnd
|
|
1309
1732
|
})
|
|
1310
1733
|
)
|
|
1311
1734
|
);
|
|
@@ -1494,11 +1917,17 @@ function registerAllTools(server2, ctx) {
|
|
|
1494
1917
|
"modifySubscriberPrepaidPackageLimits",
|
|
1495
1918
|
TOOL_SCOPES["modify_package_limits"],
|
|
1496
1919
|
ctx,
|
|
1497
|
-
async ({
|
|
1498
|
-
iccid
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1920
|
+
async ({ packageId, limits }, token2) => (
|
|
1921
|
+
// OCS accepts only packageId/newLimits/comment here, so `iccid` never
|
|
1922
|
+
// reaches the wire, and the limits nest under `newLimits`
|
|
1923
|
+
// (dataByte/mocSecond/mtcSecond/moSms/mtSms) rather than sitting flat.
|
|
1924
|
+
// mcp-stdio is a BYO-token surface, so unlike mcp-server there is no
|
|
1925
|
+
// ownership-evidence use for the ICCID here.
|
|
1926
|
+
ocsCall(ctx.env, token2, "modifySubscriberPrepaidPackageLimits", {
|
|
1927
|
+
packageId,
|
|
1928
|
+
newLimits: prepaidPackageLimits(JSON.parse(limits))
|
|
1929
|
+
})
|
|
1930
|
+
)
|
|
1502
1931
|
)
|
|
1503
1932
|
);
|
|
1504
1933
|
server2.registerTool(
|
|
@@ -1530,9 +1959,11 @@ function registerAllTools(server2, ctx) {
|
|
|
1530
1959
|
"modifySubscriberPrepaidPackageExpDate",
|
|
1531
1960
|
TOOL_SCOPES["modify_package_expiry"],
|
|
1532
1961
|
ctx,
|
|
1533
|
-
async ({
|
|
1534
|
-
const params = {
|
|
1535
|
-
if (expirationDate !== void 0)
|
|
1962
|
+
async ({ packageId, expirationDate, validity_days }, token2) => {
|
|
1963
|
+
const params = { packageId };
|
|
1964
|
+
if (expirationDate !== void 0) {
|
|
1965
|
+
params.newExpirationDate = ocsLocalDateTime(expirationDate);
|
|
1966
|
+
}
|
|
1536
1967
|
if (validity_days !== void 0) params.newValidityDuration = validity_days;
|
|
1537
1968
|
return ocsCall(ctx.env, token2, "modifySubscriberPrepaidPackageExpDate", params);
|
|
1538
1969
|
}
|
|
@@ -1556,11 +1987,24 @@ function registerAllTools(server2, ctx) {
|
|
|
1556
1987
|
"modifySubscriberPrepaidPackageStatus",
|
|
1557
1988
|
TOOL_SCOPES["modify_package_status"],
|
|
1558
1989
|
ctx,
|
|
1559
|
-
async ({
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1990
|
+
async ({ packageId, status }, token2) => {
|
|
1991
|
+
const normalised = String(status).trim().toUpperCase();
|
|
1992
|
+
if (normalised !== "ACTIVE" && normalised !== "INACTIVE") {
|
|
1993
|
+
return {
|
|
1994
|
+
isError: true,
|
|
1995
|
+
content: [
|
|
1996
|
+
{
|
|
1997
|
+
type: "text",
|
|
1998
|
+
text: `Invalid status "${status}". Use "ACTIVE" or "INACTIVE".`
|
|
1999
|
+
}
|
|
2000
|
+
]
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
return ocsCall(ctx.env, token2, "modifySubscriberPrepaidPackageStatus", {
|
|
2004
|
+
subsPrepaidPackageId: packageId,
|
|
2005
|
+
active: normalised === "ACTIVE"
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
1564
2008
|
)
|
|
1565
2009
|
);
|
|
1566
2010
|
server2.registerTool(
|
|
@@ -1581,11 +2025,29 @@ function registerAllTools(server2, ctx) {
|
|
|
1581
2025
|
"stopResumeSubsRecurringPackage",
|
|
1582
2026
|
TOOL_SCOPES["stop_resume_recurring_package"],
|
|
1583
2027
|
ctx,
|
|
1584
|
-
async ({ iccid, packageId, action }, token2) =>
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2028
|
+
async ({ iccid, packageId, action }, token2) => {
|
|
2029
|
+
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
2030
|
+
const listed = await client2.call(
|
|
2031
|
+
"listSubscriberPrepaidPackages",
|
|
2032
|
+
{ iccid }
|
|
2033
|
+
);
|
|
2034
|
+
const recurringId = recurringIdForPackage(listed, packageId);
|
|
2035
|
+
if (recurringId === void 0) {
|
|
2036
|
+
return {
|
|
2037
|
+
isError: true,
|
|
2038
|
+
content: [
|
|
2039
|
+
{
|
|
2040
|
+
type: "text",
|
|
2041
|
+
text: `Package ${packageId} is not a recurring package on ICCID ${iccid}. stop_resume_recurring_package needs a package that came from a recurring template \u2014 call list_subscriber_packages and pick one that reports a recurring subscription.`
|
|
2042
|
+
}
|
|
2043
|
+
]
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
return ocsCall(ctx.env, token2, "stopResumeSubsRecurringPackage", {
|
|
2047
|
+
recurringId,
|
|
2048
|
+
active: action === "resume"
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
1589
2051
|
)
|
|
1590
2052
|
);
|
|
1591
2053
|
server2.registerTool(
|
|
@@ -3708,7 +4170,7 @@ function registerIntelligenceTools(server2, ctx) {
|
|
|
3708
4170
|
}, async ({ accountId }) => {
|
|
3709
4171
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3710
4172
|
const resellerId = await getDefaultResellerId(ctx.env, token2).catch(() => void 0);
|
|
3711
|
-
const [statusResult, accountsResult] = await Promise.all([
|
|
4173
|
+
const [statusResult, accountsResult, resellerInfoResult] = await Promise.all([
|
|
3712
4174
|
safeCall(
|
|
3713
4175
|
ctx.env,
|
|
3714
4176
|
token2,
|
|
@@ -3720,7 +4182,17 @@ function registerIntelligenceTools(server2, ctx) {
|
|
|
3720
4182
|
token2,
|
|
3721
4183
|
"listResellerAccount",
|
|
3722
4184
|
resellerId !== void 0 ? { resellerId } : {}
|
|
3723
|
-
)
|
|
4185
|
+
),
|
|
4186
|
+
// The reseller's OWN balance. Sub-account balances cannot show that the
|
|
4187
|
+
// parent is out of credit, and that is the one state which silently
|
|
4188
|
+
// zeroes every rating group (2026-08-28: reseller 1170 at 0.00 while
|
|
4189
|
+
// fleet_health reported "require no action").
|
|
4190
|
+
resellerId !== void 0 ? safeCall(
|
|
4191
|
+
ctx.env,
|
|
4192
|
+
token2,
|
|
4193
|
+
"getResellerInfo",
|
|
4194
|
+
{ id: resellerId }
|
|
4195
|
+
) : Promise.resolve({ data: null, error: null })
|
|
3724
4196
|
]);
|
|
3725
4197
|
const sections = ["# Fleet Health Dashboard\n"];
|
|
3726
4198
|
const counts = extractEsimStatusCounts(statusResult.data);
|
|
@@ -3769,11 +4241,20 @@ High suspension rate (${totalSuspended} suspended vs ${totalActive} active)`);
|
|
|
3769
4241
|
const criticalZero = accounts.filter(
|
|
3770
4242
|
(a) => a.packageOnly === false && Number(a.balance ?? 0) <= 0
|
|
3771
4243
|
);
|
|
4244
|
+
const resellerRaw = resellerInfoResult?.data ?? null;
|
|
4245
|
+
const rawBalance = resellerRaw?.getResellerInfo?.balance;
|
|
4246
|
+
const resellerBalance = rawBalance === void 0 || rawBalance === null ? null : Number(rawBalance);
|
|
4247
|
+
const resellerOutOfCredit = resellerBalance !== null && resellerBalance <= 0;
|
|
3772
4248
|
sections.push(`
|
|
3773
4249
|
## Account Summary`);
|
|
3774
4250
|
sections.push(`- Total accounts: ${accounts.length}`);
|
|
3775
4251
|
sections.push(`- Low balance (< 10): ${lowBalance.length}`);
|
|
3776
4252
|
sections.push(`- Package-only with 0 balance: ${packageOnlyZero.length}`);
|
|
4253
|
+
if (resellerBalance !== null) {
|
|
4254
|
+
sections.push(
|
|
4255
|
+
`- Reseller balance: ${resellerBalance.toFixed(2)}` + (resellerOutOfCredit ? ` (OUT OF CREDIT - nothing can be rated)` : ``)
|
|
4256
|
+
);
|
|
4257
|
+
}
|
|
3777
4258
|
if (lowBalance.length > 0) {
|
|
3778
4259
|
sections.push(`
|
|
3779
4260
|
### Low Balance Accounts (< 10)`);
|
|
@@ -3803,8 +4284,15 @@ Action: top up non-package-only accounts at 0 balance to restore rated traffic.`
|
|
|
3803
4284
|
sections.push(`
|
|
3804
4285
|
All accounts healthy.`);
|
|
3805
4286
|
} else if (informational > 0 && warning <= 0) {
|
|
3806
|
-
|
|
4287
|
+
if (resellerOutOfCredit) {
|
|
4288
|
+
sections.push(
|
|
4289
|
+
`
|
|
4290
|
+
Action: reseller balance is ${resellerBalance.toFixed(2)}. Package-only accounts draw on their packages, but a reseller at 0 cannot rate any traffic - sessions are accepted and granted 0 bytes. Top up the reseller.`
|
|
4291
|
+
);
|
|
4292
|
+
} else {
|
|
4293
|
+
sections.push(`
|
|
3807
4294
|
Package-only accounts at 0 balance require no action.`);
|
|
4295
|
+
}
|
|
3808
4296
|
}
|
|
3809
4297
|
} else {
|
|
3810
4298
|
sections.push(`
|
|
@@ -4625,7 +5113,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4625
5113
|
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4626
5114
|
const result2 = await client2.call(
|
|
4627
5115
|
"modifySubscriberMobilePlan",
|
|
4628
|
-
{
|
|
5116
|
+
{ subscriberId: { iccid }, mobilePlanId: mobile_plan_id }
|
|
4629
5117
|
);
|
|
4630
5118
|
return {
|
|
4631
5119
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4643,6 +5131,9 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4643
5131
|
package_id: z3.number().describe("The subscriber package ID (from list_subscriber_packages)"),
|
|
4644
5132
|
start_date: z3.string().optional().describe("New start date in ISO 8601 format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss)"),
|
|
4645
5133
|
end_date: z3.string().optional().describe("New end date in ISO 8601 format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss)"),
|
|
5134
|
+
comment: z3.string().optional().describe(
|
|
5135
|
+
"Audit note stored on the OCS package record. OCS requires this field, so a default is sent when it is omitted."
|
|
5136
|
+
),
|
|
4646
5137
|
...DRY_RUN_FIELD2
|
|
4647
5138
|
},
|
|
4648
5139
|
annotations: { destructiveHint: true }
|
|
@@ -4652,11 +5143,22 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4652
5143
|
"modifySubscriberPrepaidPackageActivePeriod",
|
|
4653
5144
|
BACKLOG_TOOL_SCOPES["modify_subscriber_package_active_period"],
|
|
4654
5145
|
ctx,
|
|
4655
|
-
async ({ iccid, package_id, start_date, end_date }, token2) => {
|
|
4656
|
-
const params = { subscriber: iccid, packageId: package_id };
|
|
4657
|
-
if (start_date !== void 0) params.startDate = start_date;
|
|
4658
|
-
if (end_date !== void 0) params.endDate = end_date;
|
|
5146
|
+
async ({ iccid, package_id, start_date, end_date, comment }, token2) => {
|
|
4659
5147
|
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5148
|
+
let current;
|
|
5149
|
+
if (start_date === void 0 || end_date === void 0) {
|
|
5150
|
+
const listed = await client2.call("listSubscriberPrepaidPackages", { iccid });
|
|
5151
|
+
current = activePeriodFromPackages(listed, package_id);
|
|
5152
|
+
if (!current) {
|
|
5153
|
+
throw new Error(`Package ${package_id} is not on subscriber ${iccid}`);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const params = packageActivePeriodParams(package_id, {
|
|
5157
|
+
startDate: start_date,
|
|
5158
|
+
endDate: end_date,
|
|
5159
|
+
comment,
|
|
5160
|
+
current
|
|
5161
|
+
});
|
|
4660
5162
|
const result2 = await client2.call(
|
|
4661
5163
|
"modifySubscriberPrepaidPackageActivePeriod",
|
|
4662
5164
|
params
|
|
@@ -4688,7 +5190,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4688
5190
|
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4689
5191
|
const result2 = await client2.call(
|
|
4690
5192
|
"modifySubscriberVoipPlan",
|
|
4691
|
-
{
|
|
5193
|
+
{ subscriberId: { iccid }, voipPlanId: voip_plan_id }
|
|
4692
5194
|
);
|
|
4693
5195
|
return {
|
|
4694
5196
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4713,13 +5215,10 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4713
5215
|
BACKLOG_TOOL_SCOPES["push_steering_to_subscriber"],
|
|
4714
5216
|
ctx,
|
|
4715
5217
|
async ({ iccid }, token2) => {
|
|
4716
|
-
const cache = /* @__PURE__ */ new Map();
|
|
4717
|
-
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
4718
|
-
const subscriberId = sub.id ?? sub.subscriberId ?? iccid;
|
|
4719
5218
|
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4720
5219
|
const result2 = await client2.call(
|
|
4721
5220
|
"pushSteeringToSubs",
|
|
4722
|
-
{
|
|
5221
|
+
{ iccid }
|
|
4723
5222
|
);
|
|
4724
5223
|
return {
|
|
4725
5224
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4745,7 +5244,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4745
5244
|
ctx,
|
|
4746
5245
|
async ({ iccid }, token2) => {
|
|
4747
5246
|
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4748
|
-
const result2 = await client2.call("resetSubsGzCounter", {
|
|
5247
|
+
const result2 = await client2.call("resetSubsGzCounter", { iccid });
|
|
4749
5248
|
return {
|
|
4750
5249
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
4751
5250
|
};
|
|
@@ -4888,124 +5387,220 @@ import { z as z11 } from "zod";
|
|
|
4888
5387
|
// src/tools-ui-agent.ts
|
|
4889
5388
|
import { z as z4 } from "zod";
|
|
4890
5389
|
|
|
4891
|
-
// src/
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
};
|
|
4899
|
-
if (orgId) {
|
|
4900
|
-
try {
|
|
4901
|
-
const res = await fetch(`${baseUrl2}/organizations/${orgId}`, { headers });
|
|
4902
|
-
if (res.ok) {
|
|
4903
|
-
const org = await res.json();
|
|
4904
|
-
if (org.private_metadata?.ocs_portal?.username && org.private_metadata.ocs_portal.password) {
|
|
4905
|
-
return org.private_metadata.ocs_portal;
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
} catch {
|
|
4909
|
-
}
|
|
4910
|
-
}
|
|
4911
|
-
if (userId) {
|
|
4912
|
-
try {
|
|
4913
|
-
const res = await fetch(`${baseUrl2}/users/${userId}`, { headers });
|
|
4914
|
-
if (res.ok) {
|
|
4915
|
-
const user = await res.json();
|
|
4916
|
-
if (user.private_metadata?.ocs_portal?.username && user.private_metadata.ocs_portal.password) {
|
|
4917
|
-
return user.private_metadata.ocs_portal;
|
|
4918
|
-
}
|
|
4919
|
-
}
|
|
4920
|
-
} catch {
|
|
4921
|
-
}
|
|
4922
|
-
}
|
|
4923
|
-
return null;
|
|
4924
|
-
}
|
|
4925
|
-
|
|
4926
|
-
// src/manus-common.ts
|
|
4927
|
-
var MANUS_API_BASE = "https://api.manus.ai/v2";
|
|
4928
|
-
|
|
4929
|
-
// src/tools-ui-agent.ts
|
|
4930
|
-
async function createManusTask(apiKey, prompt, title, outputSchema) {
|
|
4931
|
-
const body2 = {
|
|
4932
|
-
message: { content: prompt },
|
|
4933
|
-
agent_profile: "manus-1.6-lite",
|
|
4934
|
-
hide_in_task_list: true,
|
|
4935
|
-
interactive_mode: false,
|
|
4936
|
-
title
|
|
4937
|
-
};
|
|
4938
|
-
if (outputSchema) {
|
|
4939
|
-
body2.structured_output_schema = outputSchema;
|
|
4940
|
-
}
|
|
4941
|
-
const res = await fetch(`${MANUS_API_BASE}/task.create`, {
|
|
4942
|
-
method: "POST",
|
|
4943
|
-
headers: {
|
|
4944
|
-
"x-manus-api-key": apiKey,
|
|
4945
|
-
"Content-Type": "application/json"
|
|
4946
|
-
},
|
|
4947
|
-
body: JSON.stringify(body2)
|
|
4948
|
-
});
|
|
4949
|
-
return await res.json();
|
|
4950
|
-
}
|
|
4951
|
-
var OCS_DASHBOARD_DEFAULT = "https://ocs.esimvault.cloud";
|
|
4952
|
-
function noCredentialsError() {
|
|
5390
|
+
// src/ui-agent-stub.ts
|
|
5391
|
+
var REMOTE_MCP_URL = "https://mcp.carrier.llc/mcp";
|
|
5392
|
+
var REASON_TEXT = {
|
|
5393
|
+
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.",
|
|
5394
|
+
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."
|
|
5395
|
+
};
|
|
5396
|
+
function uiAgentStubError(toolName, reason) {
|
|
4953
5397
|
return {
|
|
4954
5398
|
isError: true,
|
|
4955
5399
|
content: [
|
|
4956
5400
|
{
|
|
4957
5401
|
type: "text",
|
|
4958
5402
|
text: JSON.stringify({
|
|
4959
|
-
error: "
|
|
4960
|
-
|
|
4961
|
-
|
|
5403
|
+
error: "requires_worker_runtime",
|
|
5404
|
+
tool: toolName,
|
|
5405
|
+
reason,
|
|
5406
|
+
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.`,
|
|
5407
|
+
remote_url: REMOTE_MCP_URL
|
|
4962
5408
|
})
|
|
4963
5409
|
}
|
|
4964
5410
|
]
|
|
4965
5411
|
};
|
|
4966
5412
|
}
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5413
|
+
|
|
5414
|
+
// src/portal-credentials.ts
|
|
5415
|
+
var PORTAL_OPT_IN_ENV = "CARRIER_ALLOW_PORTAL_PASSWORD";
|
|
5416
|
+
var PORTAL_USERNAME_ENV = "CARRIER_OCS_PORTAL_USERNAME";
|
|
5417
|
+
var PORTAL_PASSWORD_ENV = "CARRIER_OCS_PORTAL_PASSWORD";
|
|
5418
|
+
var PORTAL_DASHBOARD_ENV = "CARRIER_OCS_DASHBOARD_URL";
|
|
5419
|
+
var PORTAL_PASSWORD_WARNING = [
|
|
5420
|
+
`${PORTAL_OPT_IN_ENV} is set: the ten OCS-dashboard tools will use the portal login in`,
|
|
5421
|
+
`${PORTAL_USERNAME_ENV} / ${PORTAL_PASSWORD_ENV}.`,
|
|
5422
|
+
"",
|
|
5423
|
+
"That password is full dashboard access, not a scoped API token. Held in an environment",
|
|
5424
|
+
"variable it is readable from shell history, from `ps` output by any process on this",
|
|
5425
|
+
"machine, and from CI logs if this runs in CI. Carrier never logs it, returns it in tool",
|
|
5426
|
+
"output, or writes it to an audit row, but it is sent to Browser Use Cloud, which types it",
|
|
5427
|
+
"into the OCS login form.",
|
|
5428
|
+
"",
|
|
5429
|
+
"Each dashboard run also costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY.",
|
|
5430
|
+
"",
|
|
5431
|
+
`Unset ${PORTAL_OPT_IN_ENV} to turn this off; the tools then refuse as they did before.`
|
|
5432
|
+
].join("\n");
|
|
5433
|
+
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.`;
|
|
5434
|
+
function flagSet(raw) {
|
|
5435
|
+
if (raw === void 0) return false;
|
|
5436
|
+
const v = raw.trim().toLowerCase();
|
|
5437
|
+
return v === "1" || v === "true";
|
|
5438
|
+
}
|
|
5439
|
+
function nonEmpty(raw) {
|
|
5440
|
+
if (raw === void 0) return void 0;
|
|
5441
|
+
return raw.length > 0 ? raw : void 0;
|
|
5442
|
+
}
|
|
5443
|
+
function readPortalOptIn(env = process.env) {
|
|
5444
|
+
const flag = flagSet(env[PORTAL_OPT_IN_ENV]);
|
|
5445
|
+
const username = nonEmpty(env[PORTAL_USERNAME_ENV]);
|
|
5446
|
+
const password = nonEmpty(env[PORTAL_PASSWORD_ENV]);
|
|
5447
|
+
const hasCredentials = username !== void 0 && password !== void 0;
|
|
5448
|
+
if (flag && hasCredentials) {
|
|
5449
|
+
return {
|
|
5450
|
+
enabled: true,
|
|
5451
|
+
credentials: { username, password },
|
|
5452
|
+
...nonEmpty(env[PORTAL_DASHBOARD_ENV]) ? { dashboardUrl: env[PORTAL_DASHBOARD_ENV] } : {}
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5455
|
+
if (flag) return { enabled: false, gap: "flag_without_credentials" };
|
|
5456
|
+
if (hasCredentials)
|
|
5457
|
+
return { enabled: false, gap: "credentials_without_flag" };
|
|
5458
|
+
return { enabled: false, gap: "not_requested" };
|
|
5459
|
+
}
|
|
5460
|
+
function warnPortalOptIn(optIn, write = (line) => console.error(line)) {
|
|
5461
|
+
if (optIn.enabled) {
|
|
5462
|
+
write(PORTAL_PASSWORD_WARNING);
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
if (optIn.gap === "flag_without_credentials") {
|
|
5466
|
+
write(PORTAL_PASSWORD_WARNING);
|
|
5467
|
+
write(
|
|
5468
|
+
`${PORTAL_USERNAME_ENV} and ${PORTAL_PASSWORD_ENV} are not both set, so the OCS-dashboard tools stay disabled.`
|
|
5469
|
+
);
|
|
5470
|
+
return;
|
|
5471
|
+
}
|
|
5472
|
+
if (optIn.gap === "credentials_without_flag") {
|
|
5473
|
+
write(
|
|
5474
|
+
`${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.`
|
|
5475
|
+
);
|
|
5476
|
+
}
|
|
4980
5477
|
}
|
|
4981
|
-
function buildLoginPreamble(dashboardUrl) {
|
|
4982
|
-
return `STEP 1 \u2014 LOGIN:
|
|
4983
|
-
Navigate to ${dashboardUrl}/login (or the main page if no /login path).
|
|
4984
|
-
Enter the OCS portal username and password provided below.
|
|
4985
|
-
Wait for the dashboard to fully load after login.
|
|
4986
|
-
If already logged in (session cookie persists), skip to STEP 2.
|
|
4987
5478
|
|
|
4988
|
-
|
|
5479
|
+
// src/ui-agent-runtime.ts
|
|
5480
|
+
function createMemoryStore() {
|
|
5481
|
+
const map = /* @__PURE__ */ new Map();
|
|
5482
|
+
return {
|
|
5483
|
+
async get(key) {
|
|
5484
|
+
const hit = map.get(key);
|
|
5485
|
+
if (!hit) return null;
|
|
5486
|
+
if (hit.expiresAt !== null && Date.now() >= hit.expiresAt) {
|
|
5487
|
+
map.delete(key);
|
|
5488
|
+
return null;
|
|
5489
|
+
}
|
|
5490
|
+
return hit.value;
|
|
5491
|
+
},
|
|
5492
|
+
async put(key, value, options) {
|
|
5493
|
+
const ttl = options?.expirationTtl;
|
|
5494
|
+
map.set(key, {
|
|
5495
|
+
value,
|
|
5496
|
+
expiresAt: ttl === void 0 ? null : Date.now() + ttl * 1e3
|
|
5497
|
+
});
|
|
5498
|
+
},
|
|
5499
|
+
async delete(key) {
|
|
5500
|
+
map.delete(key);
|
|
5501
|
+
}
|
|
5502
|
+
};
|
|
4989
5503
|
}
|
|
4990
|
-
var
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5504
|
+
var DEFAULT_MAX_RUNS_PER_PROCESS = 10;
|
|
5505
|
+
var MAX_RUNS_ENV = "CARRIER_BROWSER_USE_MAX_RUNS";
|
|
5506
|
+
var SpendLimitError = class extends Error {
|
|
5507
|
+
constructor(limit) {
|
|
5508
|
+
super(
|
|
5509
|
+
`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.`
|
|
5510
|
+
);
|
|
5511
|
+
this.limit = limit;
|
|
5512
|
+
this.name = "SpendLimitError";
|
|
5513
|
+
}
|
|
5514
|
+
limit;
|
|
5515
|
+
};
|
|
5516
|
+
function maxRunsPerProcess(env = process.env) {
|
|
5517
|
+
const raw = env[MAX_RUNS_ENV];
|
|
5518
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
5519
|
+
return DEFAULT_MAX_RUNS_PER_PROCESS;
|
|
5520
|
+
}
|
|
5521
|
+
const parsed = Number(raw);
|
|
5522
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
5523
|
+
return DEFAULT_MAX_RUNS_PER_PROCESS;
|
|
5524
|
+
}
|
|
5525
|
+
return parsed;
|
|
5526
|
+
}
|
|
5527
|
+
var RunBudget = class {
|
|
5528
|
+
constructor(limit) {
|
|
5529
|
+
this.limit = limit;
|
|
5530
|
+
}
|
|
5531
|
+
limit;
|
|
5532
|
+
used = 0;
|
|
5533
|
+
/** Consume one run, or throw. Call immediately before dispatching. */
|
|
5534
|
+
reserve() {
|
|
5535
|
+
if (this.used >= this.limit) throw new SpendLimitError(this.limit);
|
|
5536
|
+
this.used += 1;
|
|
5537
|
+
}
|
|
5538
|
+
get remaining() {
|
|
5539
|
+
return Math.max(0, this.limit - this.used);
|
|
5540
|
+
}
|
|
5541
|
+
get spent() {
|
|
5542
|
+
return this.used;
|
|
5543
|
+
}
|
|
5544
|
+
};
|
|
5545
|
+
function createUiAgentRuntime(processEnv = process.env) {
|
|
5546
|
+
const apiKey2 = processEnv.BROWSER_USE_API_KEY;
|
|
5547
|
+
const spend = { runs: 0, costUsd: 0 };
|
|
5548
|
+
const audit2 = (row) => {
|
|
5549
|
+
const cost = row.doubles?.[1];
|
|
5550
|
+
if (typeof cost === "number" && cost > 0) {
|
|
5551
|
+
spend.runs += 1;
|
|
5552
|
+
spend.costUsd = Math.round((spend.costUsd + cost) * 1e6) / 1e6;
|
|
5553
|
+
}
|
|
5554
|
+
};
|
|
5555
|
+
return {
|
|
5556
|
+
env: {
|
|
5557
|
+
...apiKey2 ? { apiKey: apiKey2 } : {},
|
|
5558
|
+
store: createMemoryStore(),
|
|
5559
|
+
audit: audit2
|
|
5560
|
+
},
|
|
5561
|
+
budget: new RunBudget(maxRunsPerProcess(processEnv)),
|
|
5562
|
+
spend,
|
|
5563
|
+
configured: Boolean(apiKey2),
|
|
5564
|
+
dispatchedTaskIds: /* @__PURE__ */ new Set()
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
// src/tools-ui-agent.ts
|
|
5569
|
+
var UI_AGENT_TOOL_SCOPES = {
|
|
5570
|
+
ui_create_steering_list: "write",
|
|
5571
|
+
ui_build_steering_list: "write",
|
|
5572
|
+
ui_set_account_steering_list: "write",
|
|
5573
|
+
ui_request_reseller_relay_change: "write",
|
|
5574
|
+
ui_create_account: "admin",
|
|
5575
|
+
ui_create_destination_list: "write",
|
|
5576
|
+
ui_edit_destination_list: "write",
|
|
5577
|
+
ui_delete_destination_list: "admin",
|
|
5578
|
+
ui_delete_package_template: "admin",
|
|
5579
|
+
ui_edit_location_zone: "write",
|
|
5580
|
+
ui_delete_location_zone: "admin"
|
|
5000
5581
|
};
|
|
5001
|
-
|
|
5582
|
+
var STDIO_NOTE_DISABLED = " Requires the remote Carrier MCP Worker deployment \u2014 the local CLI holds no OCS portal credentials.";
|
|
5583
|
+
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.";
|
|
5584
|
+
function stdioNote(optIn) {
|
|
5585
|
+
return optIn.enabled ? STDIO_NOTE_ENABLED : STDIO_NOTE_DISABLED;
|
|
5586
|
+
}
|
|
5587
|
+
function portalStub(toolName) {
|
|
5588
|
+
return uiAgentStubError(toolName, "ocs_portal_credentials");
|
|
5589
|
+
}
|
|
5590
|
+
function jsonResult(body2, isError = false) {
|
|
5591
|
+
return {
|
|
5592
|
+
...isError ? { isError: true } : {},
|
|
5593
|
+
content: [{ type: "text", text: JSON.stringify(body2, null, 2) }]
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
function wrapPortalHandler(toolName, gapId, requiredScope, ctx, runtime, optIn, buildPrompt) {
|
|
5002
5597
|
return async (args) => {
|
|
5003
5598
|
const start = Date.now();
|
|
5004
|
-
const
|
|
5599
|
+
const ocsMethod = `[ui-agent:${gapId}]`;
|
|
5005
5600
|
if (!ctx.props.scope.includes(requiredScope)) {
|
|
5006
5601
|
ctx.audit({
|
|
5007
5602
|
tool_name: toolName,
|
|
5008
|
-
ocs_method:
|
|
5603
|
+
ocs_method: ocsMethod,
|
|
5009
5604
|
status: "scope_denied",
|
|
5010
5605
|
dry_run: false,
|
|
5011
5606
|
duration_ms: 0
|
|
@@ -5020,114 +5615,130 @@ function wrapUiAgentHandler(toolName, gapId, requiredScope, ctx, buildPrompt) {
|
|
|
5020
5615
|
]
|
|
5021
5616
|
};
|
|
5022
5617
|
}
|
|
5023
|
-
if (!
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5618
|
+
if (!optIn.enabled) return portalStub(toolName);
|
|
5619
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
5620
|
+
return jsonResult(
|
|
5621
|
+
{
|
|
5622
|
+
error: "browser_use_not_configured",
|
|
5623
|
+
tool: toolName,
|
|
5624
|
+
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."
|
|
5625
|
+
},
|
|
5626
|
+
true
|
|
5627
|
+
);
|
|
5030
5628
|
}
|
|
5031
|
-
const
|
|
5032
|
-
const
|
|
5033
|
-
const
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
` + prompt + `
|
|
5041
|
-
|
|
5042
|
-
STEP 3 \u2014 VERIFICATION:
|
|
5043
|
-
After completing the operation, verify the result by checking the dashboard shows the expected state.
|
|
5044
|
-
Take a screenshot of the final state for audit purposes.
|
|
5045
|
-
Report success or failure with a clear summary.`;
|
|
5046
|
-
if (isDryRun) {
|
|
5047
|
-
const redactedPrompt = fullPrompt.replaceAll(creds.password, "***REDACTED***").replaceAll(creds.username, "***REDACTED***");
|
|
5629
|
+
const { credentials } = optIn;
|
|
5630
|
+
const dashboardUrl = optIn.dashboardUrl ?? OCS_DASHBOARD_DEFAULT;
|
|
5631
|
+
const redact = (text) => redactCredentials(text, credentials);
|
|
5632
|
+
const fullPrompt = assembleUiAgentPrompt(
|
|
5633
|
+
buildPrompt(args, dashboardUrl),
|
|
5634
|
+
credentials,
|
|
5635
|
+
dashboardUrl
|
|
5636
|
+
);
|
|
5637
|
+
if (args.dry_run === true) {
|
|
5048
5638
|
ctx.audit({
|
|
5049
5639
|
tool_name: toolName,
|
|
5050
|
-
ocs_method:
|
|
5640
|
+
ocs_method: ocsMethod,
|
|
5051
5641
|
status: "dry_run",
|
|
5052
5642
|
dry_run: true,
|
|
5053
5643
|
duration_ms: 0,
|
|
5054
5644
|
event_type: "ui_agent_dispatch"
|
|
5055
5645
|
});
|
|
5056
|
-
return {
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5646
|
+
return jsonResult({
|
|
5647
|
+
dry_run: true,
|
|
5648
|
+
tool: toolName,
|
|
5649
|
+
gap_id: gapId,
|
|
5650
|
+
agent_prompt_preview: redact(fullPrompt),
|
|
5651
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
5652
|
+
note: "No browser agent was dispatched. Set dry_run=false to execute.",
|
|
5653
|
+
portal_password_note: PORTAL_PASSWORD_RUN_NOTE
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
try {
|
|
5657
|
+
runtime.budget.reserve();
|
|
5658
|
+
} catch (err7) {
|
|
5659
|
+
if (err7 instanceof SpendLimitError) {
|
|
5660
|
+
ctx.audit({
|
|
5661
|
+
tool_name: toolName,
|
|
5662
|
+
ocs_method: ocsMethod,
|
|
5663
|
+
// The existing quota status, not a new one: AuditRow's status is a
|
|
5664
|
+
// closed union shared with the Worker.
|
|
5665
|
+
status: "quota_exceeded",
|
|
5666
|
+
dry_run: false,
|
|
5667
|
+
duration_ms: 0,
|
|
5668
|
+
event_type: "ui_agent_dispatch"
|
|
5669
|
+
});
|
|
5670
|
+
return jsonResult(
|
|
5671
|
+
{ error: "run_limit_reached", message: err7.message },
|
|
5672
|
+
true
|
|
5673
|
+
);
|
|
5674
|
+
}
|
|
5675
|
+
throw err7;
|
|
5070
5676
|
}
|
|
5071
5677
|
try {
|
|
5072
|
-
const result2 = await
|
|
5073
|
-
ctx.env.MANUS_API_KEY,
|
|
5678
|
+
const result2 = await runUiAgent(
|
|
5074
5679
|
fullPrompt,
|
|
5075
|
-
`Carrier
|
|
5076
|
-
|
|
5680
|
+
`Carrier CLI UI Agent: ${toolName} (${gapId})`,
|
|
5681
|
+
runtime.env,
|
|
5682
|
+
{ ownerSub: ctx.props.sub, redact }
|
|
5077
5683
|
);
|
|
5078
|
-
if (!result2.
|
|
5684
|
+
if (!result2.task_id) {
|
|
5079
5685
|
ctx.audit({
|
|
5080
5686
|
tool_name: toolName,
|
|
5081
|
-
ocs_method:
|
|
5687
|
+
ocs_method: ocsMethod,
|
|
5082
5688
|
status: "error",
|
|
5083
5689
|
dry_run: false,
|
|
5084
5690
|
duration_ms: Date.now() - start,
|
|
5085
5691
|
event_type: "ui_agent_dispatch"
|
|
5086
5692
|
});
|
|
5087
|
-
return
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
message: result2.error?.message ?? "Failed to create Manus task",
|
|
5095
|
-
code: result2.error?.code
|
|
5096
|
-
})
|
|
5097
|
-
}
|
|
5098
|
-
]
|
|
5099
|
-
};
|
|
5693
|
+
return jsonResult(
|
|
5694
|
+
{
|
|
5695
|
+
error: "steel_dispatch_failed",
|
|
5696
|
+
message: "Failed to create a browser agent task"
|
|
5697
|
+
},
|
|
5698
|
+
true
|
|
5699
|
+
);
|
|
5100
5700
|
}
|
|
5701
|
+
runtime.dispatchedTaskIds.add(result2.task_id);
|
|
5101
5702
|
ctx.audit({
|
|
5102
5703
|
tool_name: toolName,
|
|
5103
|
-
ocs_method:
|
|
5104
|
-
status: "ui_agent_dispatched",
|
|
5704
|
+
ocs_method: ocsMethod,
|
|
5705
|
+
status: result2.status === "error" ? "error" : "ui_agent_dispatched",
|
|
5105
5706
|
dry_run: false,
|
|
5106
5707
|
duration_ms: Date.now() - start,
|
|
5107
5708
|
event_type: "ui_agent_dispatch",
|
|
5108
5709
|
manus_task_id: result2.task_id
|
|
5710
|
+
// AE blob position is the contract; the name is history
|
|
5711
|
+
});
|
|
5712
|
+
return jsonResult({
|
|
5713
|
+
status: result2.status === "stopped" ? "ui_agent_completed" : result2.status === "waiting" ? "ui_agent_awaiting_input" : result2.status,
|
|
5714
|
+
tool: toolName,
|
|
5715
|
+
gap_id: gapId,
|
|
5716
|
+
// Held still across the vendor swap and across transports: MCP clients
|
|
5717
|
+
// and the console read these names.
|
|
5718
|
+
steel_task_id: result2.task_id,
|
|
5719
|
+
steel_session_viewer_url: result2.session_viewer_url,
|
|
5720
|
+
// Redacted defensively. `runUiAgent` already applies the same redactor
|
|
5721
|
+
// before the record is stored, so this is the second of two passes over
|
|
5722
|
+
// the fields an agent can echo its instructions into. Field-by-field
|
|
5723
|
+
// rather than over the serialised JSON: a password containing a quote
|
|
5724
|
+
// or a backslash is escaped by JSON.stringify and would no longer match
|
|
5725
|
+
// a plain substring replace.
|
|
5726
|
+
result: result2.result ? {
|
|
5727
|
+
...result2.result,
|
|
5728
|
+
summary: redact(result2.result.summary),
|
|
5729
|
+
...result2.result.entity_id !== void 0 ? { entity_id: redact(result2.result.entity_id) } : {},
|
|
5730
|
+
...result2.result.error_message !== void 0 ? { error_message: redact(result2.result.error_message) } : {}
|
|
5731
|
+
} : void 0,
|
|
5732
|
+
cost_usd: result2.spend?.costUsd,
|
|
5733
|
+
session_spend_usd: runtime.spend.costUsd,
|
|
5734
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
5735
|
+
note: "The run completed in-process \u2014 a local CLI has no background runtime, so this call blocked for its duration.",
|
|
5736
|
+
portal_password_note: PORTAL_PASSWORD_RUN_NOTE
|
|
5109
5737
|
});
|
|
5110
|
-
return {
|
|
5111
|
-
content: [
|
|
5112
|
-
{
|
|
5113
|
-
type: "text",
|
|
5114
|
-
text: JSON.stringify({
|
|
5115
|
-
status: "ui_agent_dispatched",
|
|
5116
|
-
tool: toolName,
|
|
5117
|
-
gap_id: gapId,
|
|
5118
|
-
manus_task_id: result2.task_id,
|
|
5119
|
-
manus_task_url: result2.task_url,
|
|
5120
|
-
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.",
|
|
5121
|
-
webhook_status: "active",
|
|
5122
|
-
poll_endpoint: `GET ${MANUS_API_BASE}/task.listMessages?task_id=${result2.task_id}&order=desc&limit=5`
|
|
5123
|
-
}, null, 2)
|
|
5124
|
-
}
|
|
5125
|
-
]
|
|
5126
|
-
};
|
|
5127
5738
|
} catch (err7) {
|
|
5128
5739
|
ctx.audit({
|
|
5129
5740
|
tool_name: toolName,
|
|
5130
|
-
ocs_method:
|
|
5741
|
+
ocs_method: ocsMethod,
|
|
5131
5742
|
status: "error",
|
|
5132
5743
|
dry_run: false,
|
|
5133
5744
|
duration_ms: Date.now() - start,
|
|
@@ -5138,57 +5749,44 @@ Report success or failure with a clear summary.`;
|
|
|
5138
5749
|
content: [
|
|
5139
5750
|
{
|
|
5140
5751
|
type: "text",
|
|
5141
|
-
text:
|
|
5752
|
+
text: redact(
|
|
5753
|
+
`Error dispatching the UI agent: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
5754
|
+
)
|
|
5142
5755
|
}
|
|
5143
5756
|
]
|
|
5144
5757
|
};
|
|
5145
5758
|
}
|
|
5146
5759
|
};
|
|
5147
5760
|
}
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
ui_set_account_steering_list: "write",
|
|
5152
|
-
ui_request_reseller_relay_change: "write",
|
|
5153
|
-
ui_create_account: "admin",
|
|
5154
|
-
ui_create_destination_list: "write",
|
|
5155
|
-
ui_edit_destination_list: "write",
|
|
5156
|
-
ui_delete_destination_list: "admin",
|
|
5157
|
-
ui_delete_package_template: "admin",
|
|
5158
|
-
ui_edit_location_zone: "write",
|
|
5159
|
-
ui_delete_location_zone: "admin"
|
|
5160
|
-
};
|
|
5161
|
-
function registerAllUiAgentTools(server2, ctx) {
|
|
5761
|
+
function registerAllUiAgentTools(server2, ctx, runtime, optIn = readPortalOptIn()) {
|
|
5762
|
+
const NOTE = stdioNote(optIn);
|
|
5763
|
+
const gated = (toolName, gapId, scope, buildPrompt) => wrapPortalHandler(toolName, gapId, scope, ctx, runtime, optIn, buildPrompt);
|
|
5162
5764
|
server2.registerTool(
|
|
5163
5765
|
"ui_create_steering_list",
|
|
5164
5766
|
{
|
|
5165
5767
|
title: "Create Steering List (UI Agent)",
|
|
5166
|
-
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
|
|
5768
|
+
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,
|
|
5167
5769
|
inputSchema: {
|
|
5168
5770
|
name: z4.string().describe("Name for the new steering list"),
|
|
5169
5771
|
description: z4.string().optional().describe("Optional description for the steering list"),
|
|
5170
5772
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5171
5773
|
}
|
|
5172
5774
|
},
|
|
5173
|
-
|
|
5775
|
+
gated(
|
|
5174
5776
|
"ui_create_steering_list",
|
|
5175
5777
|
"G-01",
|
|
5176
5778
|
"write",
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
` + (args.description ? ` Description: ${args.description}
|
|
5182
|
-
` : "") + `Click the "Create" or "Add" button to create the steering list.
|
|
5183
|
-
After creation, note the new steering list ID from the dashboard.
|
|
5184
|
-
`
|
|
5779
|
+
(args, dashboardUrl) => buildCreateSteeringListPrompt(
|
|
5780
|
+
args,
|
|
5781
|
+
dashboardUrl
|
|
5782
|
+
)
|
|
5185
5783
|
)
|
|
5186
5784
|
);
|
|
5187
5785
|
server2.registerTool(
|
|
5188
5786
|
"ui_build_steering_list",
|
|
5189
5787
|
{
|
|
5190
5788
|
title: "Build Steering List (UI Agent)",
|
|
5191
|
-
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').",
|
|
5789
|
+
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,
|
|
5192
5790
|
inputSchema: {
|
|
5193
5791
|
steering_list_id: z4.number().describe("ID of the steering list to modify"),
|
|
5194
5792
|
add_operators: z4.array(z4.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
|
|
@@ -5197,42 +5795,35 @@ After creation, note the new steering list ID from the dashboard.
|
|
|
5197
5795
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5198
5796
|
}
|
|
5199
5797
|
},
|
|
5200
|
-
|
|
5798
|
+
gated(
|
|
5201
5799
|
"ui_build_steering_list",
|
|
5202
5800
|
"G-02",
|
|
5203
5801
|
"write",
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
` + (args.add_operators && args.add_operators.length > 0 ? `Add the following operators: ${args.add_operators.join(", ")}
|
|
5209
|
-
` : "") + (args.remove_operators && args.remove_operators.length > 0 ? `Remove the following operators: ${args.remove_operators.join(", ")}
|
|
5210
|
-
` : "") + `Save the changes and verify the updated operator list.
|
|
5211
|
-
`
|
|
5802
|
+
(args, dashboardUrl) => buildBuildSteeringListPrompt(
|
|
5803
|
+
args,
|
|
5804
|
+
dashboardUrl
|
|
5805
|
+
)
|
|
5212
5806
|
)
|
|
5213
5807
|
);
|
|
5214
5808
|
server2.registerTool(
|
|
5215
5809
|
"ui_set_account_steering_list",
|
|
5216
5810
|
{
|
|
5217
5811
|
title: "Set Account Steering List (UI Agent)",
|
|
5218
|
-
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).",
|
|
5812
|
+
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,
|
|
5219
5813
|
inputSchema: {
|
|
5220
5814
|
account_id: z4.number().describe("Account ID to assign the steering list to"),
|
|
5221
5815
|
steering_list_id: z4.number().describe("Steering list ID to assign (0 to remove/unset)"),
|
|
5222
5816
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5223
5817
|
}
|
|
5224
5818
|
},
|
|
5225
|
-
|
|
5819
|
+
gated(
|
|
5226
5820
|
"ui_set_account_steering_list",
|
|
5227
5821
|
"G-03",
|
|
5228
5822
|
"write",
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
` : `Assign steering list ID ${args.steering_list_id} to this account.
|
|
5234
|
-
`) + `Save the changes and verify the steering list assignment is updated.
|
|
5235
|
-
`
|
|
5823
|
+
(args, dashboardUrl) => buildSetAccountSteeringListPrompt(
|
|
5824
|
+
args,
|
|
5825
|
+
dashboardUrl
|
|
5826
|
+
)
|
|
5236
5827
|
)
|
|
5237
5828
|
);
|
|
5238
5829
|
server2.registerTool(
|
|
@@ -5252,7 +5843,12 @@ Open account ID ${args.account_id}.
|
|
|
5252
5843
|
if (!ctx.props.scope.includes("write")) {
|
|
5253
5844
|
return {
|
|
5254
5845
|
isError: true,
|
|
5255
|
-
content: [
|
|
5846
|
+
content: [
|
|
5847
|
+
{
|
|
5848
|
+
type: "text",
|
|
5849
|
+
text: "Scope denied: tool 'ui_request_reseller_relay_change' requires 'write' scope."
|
|
5850
|
+
}
|
|
5851
|
+
]
|
|
5256
5852
|
};
|
|
5257
5853
|
}
|
|
5258
5854
|
const resellerId = args.reseller_id ?? ctx.props.reseller_id;
|
|
@@ -5264,7 +5860,15 @@ Open account ID ${args.account_id}.
|
|
|
5264
5860
|
if (requested.length === 0) {
|
|
5265
5861
|
return {
|
|
5266
5862
|
isError: true,
|
|
5267
|
-
content: [
|
|
5863
|
+
content: [
|
|
5864
|
+
{
|
|
5865
|
+
type: "text",
|
|
5866
|
+
text: JSON.stringify({
|
|
5867
|
+
error: "relay_state_required",
|
|
5868
|
+
message: "Choose at least one relay flag and desired state."
|
|
5869
|
+
})
|
|
5870
|
+
}
|
|
5871
|
+
]
|
|
5268
5872
|
};
|
|
5269
5873
|
}
|
|
5270
5874
|
const draft = [
|
|
@@ -5284,14 +5888,23 @@ Open account ID ${args.account_id}.
|
|
|
5284
5888
|
event_type: "ui_agent_dispatch"
|
|
5285
5889
|
});
|
|
5286
5890
|
return {
|
|
5287
|
-
content: [
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5891
|
+
content: [
|
|
5892
|
+
{
|
|
5893
|
+
type: "text",
|
|
5894
|
+
text: JSON.stringify(
|
|
5895
|
+
{
|
|
5896
|
+
status: "parent_request_drafted",
|
|
5897
|
+
capability: "parent_controlled",
|
|
5898
|
+
reseller_id: resellerId,
|
|
5899
|
+
parent_reseller: resellerId === 1170 ? "Bridge4IP" : "parent reseller/support",
|
|
5900
|
+
draft,
|
|
5901
|
+
sent: false
|
|
5902
|
+
},
|
|
5903
|
+
null,
|
|
5904
|
+
2
|
|
5905
|
+
)
|
|
5906
|
+
}
|
|
5907
|
+
]
|
|
5295
5908
|
};
|
|
5296
5909
|
}
|
|
5297
5910
|
);
|
|
@@ -5299,7 +5912,7 @@ Open account ID ${args.account_id}.
|
|
|
5299
5912
|
"ui_create_account",
|
|
5300
5913
|
{
|
|
5301
5914
|
title: "Create Account (UI Agent)",
|
|
5302
|
-
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).",
|
|
5915
|
+
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,
|
|
5303
5916
|
inputSchema: {
|
|
5304
5917
|
name: z4.string().describe("Name for the new account"),
|
|
5305
5918
|
description: z4.string().optional().describe("Optional description"),
|
|
@@ -5307,26 +5920,21 @@ Open account ID ${args.account_id}.
|
|
|
5307
5920
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5308
5921
|
}
|
|
5309
5922
|
},
|
|
5310
|
-
|
|
5923
|
+
gated(
|
|
5311
5924
|
"ui_create_account",
|
|
5312
5925
|
"G-05",
|
|
5313
5926
|
"admin",
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
` + (args.description ? ` Description: ${args.description}
|
|
5319
|
-
` : "") + (args.initial_balance ? ` Initial balance: ${args.initial_balance}
|
|
5320
|
-
` : "") + `Click the "Create" or "Add" button.
|
|
5321
|
-
After creation, note the new account ID from the dashboard.
|
|
5322
|
-
`
|
|
5927
|
+
(args, dashboardUrl) => buildCreateAccountPrompt(
|
|
5928
|
+
args,
|
|
5929
|
+
dashboardUrl
|
|
5930
|
+
)
|
|
5323
5931
|
)
|
|
5324
5932
|
);
|
|
5325
5933
|
server2.registerTool(
|
|
5326
5934
|
"ui_create_destination_list",
|
|
5327
5935
|
{
|
|
5328
5936
|
title: "Create Destination List (UI Agent)",
|
|
5329
|
-
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`.",
|
|
5937
|
+
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,
|
|
5330
5938
|
inputSchema: {
|
|
5331
5939
|
name: z4.string().describe("Name for the new destination list"),
|
|
5332
5940
|
prefixes: z4.array(z4.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
|
|
@@ -5334,25 +5942,21 @@ After creation, note the new account ID from the dashboard.
|
|
|
5334
5942
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5335
5943
|
}
|
|
5336
5944
|
},
|
|
5337
|
-
|
|
5945
|
+
gated(
|
|
5338
5946
|
"ui_create_destination_list",
|
|
5339
5947
|
"G-12",
|
|
5340
5948
|
"write",
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
` + (args.description ? ` Description: ${args.description}
|
|
5346
|
-
` : "") + (args.prefixes && args.prefixes.length > 0 ? ` Prefixes to add: ${args.prefixes.join(", ")}
|
|
5347
|
-
` : "") + `Save the new destination list and note the ID.
|
|
5348
|
-
`
|
|
5949
|
+
(args, dashboardUrl) => buildCreateDestinationListPrompt(
|
|
5950
|
+
args,
|
|
5951
|
+
dashboardUrl
|
|
5952
|
+
)
|
|
5349
5953
|
)
|
|
5350
5954
|
);
|
|
5351
5955
|
server2.registerTool(
|
|
5352
5956
|
"ui_edit_destination_list",
|
|
5353
5957
|
{
|
|
5354
5958
|
title: "Edit Destination List (UI Agent)",
|
|
5355
|
-
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`.",
|
|
5959
|
+
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`." + NOTE,
|
|
5356
5960
|
inputSchema: {
|
|
5357
5961
|
destination_list_id: z4.number().describe("ID of the destination list to edit"),
|
|
5358
5962
|
add_prefixes: z4.array(z4.string()).optional().describe("Prefixes to add"),
|
|
@@ -5361,69 +5965,61 @@ Create a new destination list with the following details:
|
|
|
5361
5965
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5362
5966
|
}
|
|
5363
5967
|
},
|
|
5364
|
-
|
|
5968
|
+
gated(
|
|
5365
5969
|
"ui_edit_destination_list",
|
|
5366
5970
|
"G-12",
|
|
5367
5971
|
"write",
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
` : "") + (args.add_prefixes && args.add_prefixes.length > 0 ? `Add prefixes: ${args.add_prefixes.join(", ")}
|
|
5373
|
-
` : "") + (args.remove_prefixes && args.remove_prefixes.length > 0 ? `Remove prefixes: ${args.remove_prefixes.join(", ")}
|
|
5374
|
-
` : "") + `Save the changes and verify the updated prefix list.
|
|
5375
|
-
`
|
|
5972
|
+
(args, dashboardUrl) => buildEditDestinationListPrompt(
|
|
5973
|
+
args,
|
|
5974
|
+
dashboardUrl
|
|
5975
|
+
)
|
|
5376
5976
|
)
|
|
5377
5977
|
);
|
|
5378
5978
|
server2.registerTool(
|
|
5379
5979
|
"ui_delete_destination_list",
|
|
5380
5980
|
{
|
|
5381
5981
|
title: "Delete Destination List (UI Agent)",
|
|
5382
|
-
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone.",
|
|
5982
|
+
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone." + NOTE,
|
|
5383
5983
|
inputSchema: {
|
|
5384
5984
|
destination_list_id: z4.number().describe("ID of the destination list to delete"),
|
|
5385
5985
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5386
5986
|
}
|
|
5387
5987
|
},
|
|
5388
|
-
|
|
5988
|
+
gated(
|
|
5389
5989
|
"ui_delete_destination_list",
|
|
5390
5990
|
"G-12",
|
|
5391
5991
|
"admin",
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
Verify the list no longer appears in the dashboard.
|
|
5397
|
-
`
|
|
5992
|
+
(args, dashboardUrl) => buildDeleteDestinationListPrompt(
|
|
5993
|
+
args,
|
|
5994
|
+
dashboardUrl
|
|
5995
|
+
)
|
|
5398
5996
|
)
|
|
5399
5997
|
);
|
|
5400
5998
|
server2.registerTool(
|
|
5401
5999
|
"ui_delete_package_template",
|
|
5402
6000
|
{
|
|
5403
6001
|
title: "Delete Package Template (UI Agent)",
|
|
5404
|
-
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.",
|
|
6002
|
+
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,
|
|
5405
6003
|
inputSchema: {
|
|
5406
6004
|
template_id: z4.number().describe("ID of the package template to delete"),
|
|
5407
6005
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5408
6006
|
}
|
|
5409
6007
|
},
|
|
5410
|
-
|
|
6008
|
+
gated(
|
|
5411
6009
|
"ui_delete_package_template",
|
|
5412
6010
|
"G-18",
|
|
5413
6011
|
"admin",
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
Verify the template no longer appears in the template list.
|
|
5419
|
-
`
|
|
6012
|
+
(args, dashboardUrl) => buildDeletePackageTemplatePrompt(
|
|
6013
|
+
args,
|
|
6014
|
+
dashboardUrl
|
|
6015
|
+
)
|
|
5420
6016
|
)
|
|
5421
6017
|
);
|
|
5422
6018
|
server2.registerTool(
|
|
5423
6019
|
"ui_edit_location_zone",
|
|
5424
6020
|
{
|
|
5425
6021
|
title: "Edit Location Zone (UI Agent)",
|
|
5426
|
-
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`.",
|
|
6022
|
+
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,
|
|
5427
6023
|
inputSchema: {
|
|
5428
6024
|
zone_id: z4.number().describe("ID of the location zone to edit"),
|
|
5429
6025
|
new_name: z4.string().optional().describe("Rename the location zone"),
|
|
@@ -5432,137 +6028,40 @@ Verify the template no longer appears in the template list.
|
|
|
5432
6028
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5433
6029
|
}
|
|
5434
6030
|
},
|
|
5435
|
-
|
|
6031
|
+
gated(
|
|
5436
6032
|
"ui_edit_location_zone",
|
|
5437
6033
|
"G-19",
|
|
5438
6034
|
"write",
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
` : "") + (args.add_countries && args.add_countries.length > 0 ? `Add countries: ${args.add_countries.join(", ")}
|
|
5444
|
-
` : "") + (args.remove_countries && args.remove_countries.length > 0 ? `Remove countries: ${args.remove_countries.join(", ")}
|
|
5445
|
-
` : "") + `Save the changes and verify the updated country list.
|
|
5446
|
-
`
|
|
6035
|
+
(args, dashboardUrl) => buildEditLocationZonePrompt(
|
|
6036
|
+
args,
|
|
6037
|
+
dashboardUrl
|
|
6038
|
+
)
|
|
5447
6039
|
)
|
|
5448
6040
|
);
|
|
5449
6041
|
server2.registerTool(
|
|
5450
6042
|
"ui_delete_location_zone",
|
|
5451
6043
|
{
|
|
5452
6044
|
title: "Delete Location Zone (UI Agent)",
|
|
5453
|
-
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.",
|
|
6045
|
+
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,
|
|
5454
6046
|
inputSchema: {
|
|
5455
6047
|
zone_id: z4.number().describe("ID of the location zone to delete"),
|
|
5456
6048
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5457
6049
|
}
|
|
5458
6050
|
},
|
|
5459
|
-
|
|
6051
|
+
gated(
|
|
5460
6052
|
"ui_delete_location_zone",
|
|
5461
6053
|
"G-19",
|
|
5462
6054
|
"admin",
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
If the dashboard shows an error (e.g. zone in use by active templates), report the error.
|
|
5468
|
-
Verify the zone no longer appears in the zone list.
|
|
5469
|
-
`
|
|
6055
|
+
(args, dashboardUrl) => buildDeleteLocationZonePrompt(
|
|
6056
|
+
args,
|
|
6057
|
+
dashboardUrl
|
|
6058
|
+
)
|
|
5470
6059
|
)
|
|
5471
6060
|
);
|
|
5472
6061
|
}
|
|
5473
6062
|
|
|
5474
6063
|
// src/tools-ui-agent-ask.ts
|
|
5475
6064
|
import { z as z5 } from "zod";
|
|
5476
|
-
|
|
5477
|
-
// src/manus-webhook.ts
|
|
5478
|
-
var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
5479
|
-
var DEDUP_TTL_SECONDS = 7 * 24 * 3600;
|
|
5480
|
-
var PENDING_ASK_TTL_SECONDS = 24 * 3600;
|
|
5481
|
-
var PENDING_ASK_PREFIX = "manus_pending_ask:";
|
|
5482
|
-
|
|
5483
|
-
// src/manus-client.ts
|
|
5484
|
-
var MANUS_API_BASE2 = "https://api.manus.ai/v2";
|
|
5485
|
-
function getManusKeys(env) {
|
|
5486
|
-
if (!env.MANUS_API_KEY) return null;
|
|
5487
|
-
return {
|
|
5488
|
-
primary: env.MANUS_API_KEY,
|
|
5489
|
-
fallback: env.MANUS_API_KEY_FALLBACK
|
|
5490
|
-
};
|
|
5491
|
-
}
|
|
5492
|
-
var FALLBACK_TRIGGER_CODES = /* @__PURE__ */ new Set([401, 403, 429]);
|
|
5493
|
-
var ManusApiError = class extends Error {
|
|
5494
|
-
constructor(message, httpStatus, manusError, keyUsed, bothFailed) {
|
|
5495
|
-
super(message);
|
|
5496
|
-
this.httpStatus = httpStatus;
|
|
5497
|
-
this.manusError = manusError;
|
|
5498
|
-
this.keyUsed = keyUsed;
|
|
5499
|
-
this.bothFailed = bothFailed;
|
|
5500
|
-
this.name = "ManusApiError";
|
|
5501
|
-
}
|
|
5502
|
-
httpStatus;
|
|
5503
|
-
manusError;
|
|
5504
|
-
keyUsed;
|
|
5505
|
-
bothFailed;
|
|
5506
|
-
};
|
|
5507
|
-
async function withFallback(fn, keys) {
|
|
5508
|
-
const primaryResult = await fn(keys.primary);
|
|
5509
|
-
if (!FALLBACK_TRIGGER_CODES.has(primaryResult._httpStatus)) {
|
|
5510
|
-
return { ...primaryResult, key_used: "primary" };
|
|
5511
|
-
}
|
|
5512
|
-
if (!keys.fallback) {
|
|
5513
|
-
throw new ManusApiError(
|
|
5514
|
-
`Manus API request failed: HTTP ${primaryResult._httpStatus}`,
|
|
5515
|
-
primaryResult._httpStatus,
|
|
5516
|
-
void 0,
|
|
5517
|
-
"primary",
|
|
5518
|
-
false
|
|
5519
|
-
);
|
|
5520
|
-
}
|
|
5521
|
-
const fallbackResult = await fn(keys.fallback);
|
|
5522
|
-
if (!FALLBACK_TRIGGER_CODES.has(fallbackResult._httpStatus)) {
|
|
5523
|
-
return { ...fallbackResult, key_used: "fallback" };
|
|
5524
|
-
}
|
|
5525
|
-
throw new ManusApiError(
|
|
5526
|
-
`Manus API request failed with both keys: HTTP ${fallbackResult._httpStatus}`,
|
|
5527
|
-
fallbackResult._httpStatus,
|
|
5528
|
-
void 0,
|
|
5529
|
-
"fallback",
|
|
5530
|
-
true
|
|
5531
|
-
);
|
|
5532
|
-
}
|
|
5533
|
-
async function manusPost(path, apiKey, body2) {
|
|
5534
|
-
const res = await fetch(`${MANUS_API_BASE2}/${path}`, {
|
|
5535
|
-
method: "POST",
|
|
5536
|
-
headers: {
|
|
5537
|
-
"x-manus-api-key": apiKey,
|
|
5538
|
-
"Content-Type": "application/json"
|
|
5539
|
-
},
|
|
5540
|
-
body: JSON.stringify(body2)
|
|
5541
|
-
});
|
|
5542
|
-
const data = await res.json();
|
|
5543
|
-
return { _httpStatus: res.status, data, key_used: "primary" };
|
|
5544
|
-
}
|
|
5545
|
-
async function sendMessage(keys, taskId, content, opts) {
|
|
5546
|
-
const message = { content };
|
|
5547
|
-
if (opts?.connectors?.length) message.connectors = opts.connectors;
|
|
5548
|
-
if (opts?.enableSkills?.length) message.enable_skills = opts.enableSkills;
|
|
5549
|
-
if (opts?.forceSkills?.length) message.force_skills = opts.forceSkills;
|
|
5550
|
-
const body2 = {
|
|
5551
|
-
task_id: taskId,
|
|
5552
|
-
message
|
|
5553
|
-
};
|
|
5554
|
-
if (opts?.agentProfile) body2.agent_profile = opts.agentProfile;
|
|
5555
|
-
if (opts?.outputSchema) body2.structured_output_schema = opts.outputSchema;
|
|
5556
|
-
return withFallback(
|
|
5557
|
-
(apiKey) => manusPost("task.sendMessage", apiKey, body2),
|
|
5558
|
-
keys
|
|
5559
|
-
);
|
|
5560
|
-
}
|
|
5561
|
-
async function askReply(keys, taskId, reply) {
|
|
5562
|
-
return sendMessage(keys, taskId, reply);
|
|
5563
|
-
}
|
|
5564
|
-
|
|
5565
|
-
// src/tools-ui-agent-ask.ts
|
|
5566
6065
|
var UI_AGENT_ASK_TOOL_SCOPES = {
|
|
5567
6066
|
ui_agent_reply: "write",
|
|
5568
6067
|
ui_agent_list_pending: "read"
|
|
@@ -5572,12 +6071,23 @@ function computeExpiresAt(askedAt) {
|
|
|
5572
6071
|
if (isNaN(asked)) return "";
|
|
5573
6072
|
return new Date(asked + PENDING_ASK_TTL_SECONDS * 1e3).toISOString();
|
|
5574
6073
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
6074
|
+
var emptyStore = {
|
|
6075
|
+
get: async () => null,
|
|
6076
|
+
put: async () => void 0,
|
|
6077
|
+
delete: async () => void 0
|
|
6078
|
+
};
|
|
6079
|
+
async function callerOwnsTask(ctx, taskId, fallback) {
|
|
6080
|
+
const key = `steel_task:${taskId}`;
|
|
6081
|
+
let raw = null;
|
|
5577
6082
|
try {
|
|
5578
|
-
raw = await ctx.env.CARRIER_USERS.get(
|
|
6083
|
+
raw = await ctx.env.CARRIER_USERS.get(key);
|
|
5579
6084
|
} catch {
|
|
5580
|
-
return false;
|
|
6085
|
+
if (!fallback) return false;
|
|
6086
|
+
try {
|
|
6087
|
+
raw = await fallback.get(key);
|
|
6088
|
+
} catch {
|
|
6089
|
+
return false;
|
|
6090
|
+
}
|
|
5581
6091
|
}
|
|
5582
6092
|
if (!raw) return false;
|
|
5583
6093
|
try {
|
|
@@ -5587,15 +6097,26 @@ async function callerOwnsTask(ctx, taskId) {
|
|
|
5587
6097
|
return false;
|
|
5588
6098
|
}
|
|
5589
6099
|
}
|
|
5590
|
-
function
|
|
6100
|
+
function listableBinding(ctx) {
|
|
6101
|
+
const kv = ctx.env.CARRIER_USERS;
|
|
6102
|
+
return kv && typeof kv.list === "function" ? kv : null;
|
|
6103
|
+
}
|
|
6104
|
+
function registerUiAgentAskTools(server2, ctx, runtime) {
|
|
6105
|
+
const sharedEnv = () => ({
|
|
6106
|
+
...runtime?.env.apiKey ? { apiKey: runtime.env.apiKey } : {},
|
|
6107
|
+
store: ctx.env.CARRIER_USERS ?? runtime?.env.store ?? emptyStore,
|
|
6108
|
+
audit: runtime?.env.audit ?? (() => void 0)
|
|
6109
|
+
});
|
|
5591
6110
|
server2.registerTool(
|
|
5592
6111
|
"ui_agent_reply",
|
|
5593
6112
|
{
|
|
5594
6113
|
title: "Reply to Paused UI Agent Task",
|
|
5595
|
-
description: "Resumes a
|
|
6114
|
+
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.",
|
|
5596
6115
|
inputSchema: {
|
|
5597
|
-
task_id: z5.string().describe("
|
|
5598
|
-
reply: z5.string().describe(
|
|
6116
|
+
task_id: z5.string().describe("Task ID to resume (from ui_agent_list_pending)"),
|
|
6117
|
+
reply: z5.string().describe(
|
|
6118
|
+
"Your answer to the agent's question (e.g. a 2FA code or confirmation)"
|
|
6119
|
+
)
|
|
5599
6120
|
}
|
|
5600
6121
|
},
|
|
5601
6122
|
async (args) => {
|
|
@@ -5619,106 +6140,63 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5619
6140
|
]
|
|
5620
6141
|
};
|
|
5621
6142
|
}
|
|
5622
|
-
const
|
|
5623
|
-
|
|
6143
|
+
const env = sharedEnv();
|
|
6144
|
+
let pendingRaw = null;
|
|
6145
|
+
try {
|
|
6146
|
+
pendingRaw = await env.store.get(`${PENDING_ASK_PREFIX}${task_id}`);
|
|
6147
|
+
} catch {
|
|
6148
|
+
pendingRaw = null;
|
|
6149
|
+
}
|
|
6150
|
+
if (pendingRaw === null || !await callerOwnsTask(ctx, task_id, runtime?.env.store)) {
|
|
5624
6151
|
return {
|
|
5625
6152
|
isError: true,
|
|
5626
6153
|
content: [
|
|
5627
6154
|
{
|
|
5628
6155
|
type: "text",
|
|
5629
6156
|
text: JSON.stringify({
|
|
5630
|
-
error: "
|
|
5631
|
-
message:
|
|
6157
|
+
error: "task_not_pending",
|
|
6158
|
+
message: `No pending-ask entry found for task_id=${task_id}. The task may have already been resumed, completed, or expired (24h TTL). Use ui_agent_list_pending to see currently waiting tasks.`
|
|
5632
6159
|
})
|
|
5633
6160
|
}
|
|
5634
6161
|
]
|
|
5635
6162
|
};
|
|
5636
6163
|
}
|
|
5637
|
-
const
|
|
5638
|
-
|
|
5639
|
-
|
|
6164
|
+
const outcome = await resumeUiAgentTask(task_id, reply, env);
|
|
6165
|
+
ctx.audit({
|
|
6166
|
+
tool_name: "ui_agent_reply",
|
|
6167
|
+
ocs_method: "[ui-agent:ask-reply]",
|
|
6168
|
+
status: outcome.ok ? "ui_agent_resumed" : "error",
|
|
6169
|
+
dry_run: false,
|
|
6170
|
+
duration_ms: Date.now() - start,
|
|
6171
|
+
event_type: "ui_agent_resume"
|
|
6172
|
+
});
|
|
6173
|
+
if (!outcome.ok) {
|
|
5640
6174
|
return {
|
|
5641
6175
|
isError: true,
|
|
5642
6176
|
content: [
|
|
5643
6177
|
{
|
|
5644
6178
|
type: "text",
|
|
5645
6179
|
text: JSON.stringify({
|
|
5646
|
-
error: "
|
|
5647
|
-
message:
|
|
6180
|
+
error: "resume_failed",
|
|
6181
|
+
message: outcome.error ?? "The task could not be resumed."
|
|
5648
6182
|
})
|
|
5649
6183
|
}
|
|
5650
6184
|
]
|
|
5651
6185
|
};
|
|
5652
6186
|
}
|
|
5653
|
-
let result2;
|
|
5654
|
-
try {
|
|
5655
|
-
const replyOutcome = await askReply(keys, task_id, reply);
|
|
5656
|
-
result2 = replyOutcome.data;
|
|
5657
|
-
} catch (err7) {
|
|
5658
|
-
ctx.audit({
|
|
5659
|
-
tool_name: "ui_agent_reply",
|
|
5660
|
-
ocs_method: "[ui-agent:ask-reply]",
|
|
5661
|
-
status: "error",
|
|
5662
|
-
dry_run: false,
|
|
5663
|
-
duration_ms: Date.now() - start,
|
|
5664
|
-
event_type: "ui_agent_resume",
|
|
5665
|
-
manus_task_id: task_id
|
|
5666
|
-
});
|
|
5667
|
-
return {
|
|
5668
|
-
isError: true,
|
|
5669
|
-
content: [
|
|
5670
|
-
{
|
|
5671
|
-
type: "text",
|
|
5672
|
-
text: `Error calling Manus task.reply: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
5673
|
-
}
|
|
5674
|
-
]
|
|
5675
|
-
};
|
|
5676
|
-
}
|
|
5677
|
-
if (!result2.ok) {
|
|
5678
|
-
ctx.audit({
|
|
5679
|
-
tool_name: "ui_agent_reply",
|
|
5680
|
-
ocs_method: "[ui-agent:ask-reply]",
|
|
5681
|
-
status: "error",
|
|
5682
|
-
dry_run: false,
|
|
5683
|
-
duration_ms: Date.now() - start,
|
|
5684
|
-
event_type: "ui_agent_resume",
|
|
5685
|
-
manus_task_id: task_id
|
|
5686
|
-
});
|
|
5687
|
-
return {
|
|
5688
|
-
isError: true,
|
|
5689
|
-
content: [
|
|
5690
|
-
{
|
|
5691
|
-
type: "text",
|
|
5692
|
-
text: JSON.stringify({
|
|
5693
|
-
error: "manus_reply_failed",
|
|
5694
|
-
message: result2.error?.message ?? "Manus task.reply returned ok=false",
|
|
5695
|
-
code: result2.error?.code,
|
|
5696
|
-
task_id
|
|
5697
|
-
})
|
|
5698
|
-
}
|
|
5699
|
-
]
|
|
5700
|
-
};
|
|
5701
|
-
}
|
|
5702
|
-
ctx.audit({
|
|
5703
|
-
tool_name: "ui_agent_reply",
|
|
5704
|
-
ocs_method: "[ui-agent:ask-reply]",
|
|
5705
|
-
status: "ui_agent_resumed",
|
|
5706
|
-
dry_run: false,
|
|
5707
|
-
duration_ms: Date.now() - start,
|
|
5708
|
-
event_type: "ui_agent_resume",
|
|
5709
|
-
manus_task_id: task_id
|
|
5710
|
-
});
|
|
5711
|
-
await ctx.env.CARRIER_USERS.delete(pendingKey);
|
|
5712
6187
|
return {
|
|
5713
6188
|
content: [
|
|
5714
6189
|
{
|
|
5715
6190
|
type: "text",
|
|
5716
|
-
text: JSON.stringify(
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
6191
|
+
text: JSON.stringify(
|
|
6192
|
+
{
|
|
6193
|
+
status: "ui_agent_resumed",
|
|
6194
|
+
task_id,
|
|
6195
|
+
reply_length: reply.length
|
|
6196
|
+
},
|
|
6197
|
+
null,
|
|
6198
|
+
2
|
|
6199
|
+
)
|
|
5722
6200
|
}
|
|
5723
6201
|
]
|
|
5724
6202
|
};
|
|
@@ -5728,7 +6206,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5728
6206
|
"ui_agent_list_pending",
|
|
5729
6207
|
{
|
|
5730
6208
|
title: "List Pending UI Agent Tasks (Waiting for Input)",
|
|
5731
|
-
description: "Returns
|
|
6209
|
+
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.",
|
|
5732
6210
|
inputSchema: {}
|
|
5733
6211
|
},
|
|
5734
6212
|
async () => {
|
|
@@ -5743,61 +6221,61 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5743
6221
|
]
|
|
5744
6222
|
};
|
|
5745
6223
|
}
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
pending_tasks: [],
|
|
5768
|
-
count: 0,
|
|
5769
|
-
message: "No Manus tasks are currently waiting for input."
|
|
5770
|
-
}, null, 2)
|
|
5771
|
-
}
|
|
5772
|
-
]
|
|
5773
|
-
};
|
|
6224
|
+
const kv = listableBinding(ctx);
|
|
6225
|
+
let taskIds;
|
|
6226
|
+
if (kv) {
|
|
6227
|
+
try {
|
|
6228
|
+
const listing = await kv.list({ prefix: PENDING_ASK_PREFIX });
|
|
6229
|
+
taskIds = listing.keys.map(
|
|
6230
|
+
(k) => k.name.slice(PENDING_ASK_PREFIX.length)
|
|
6231
|
+
);
|
|
6232
|
+
} catch (err7) {
|
|
6233
|
+
return {
|
|
6234
|
+
isError: true,
|
|
6235
|
+
content: [
|
|
6236
|
+
{
|
|
6237
|
+
type: "text",
|
|
6238
|
+
text: `Error listing pending tasks: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
6239
|
+
}
|
|
6240
|
+
]
|
|
6241
|
+
};
|
|
6242
|
+
}
|
|
6243
|
+
} else {
|
|
6244
|
+
taskIds = [...runtime?.dispatchedTaskIds ?? []];
|
|
5774
6245
|
}
|
|
6246
|
+
const store = sharedEnv().store;
|
|
5775
6247
|
const entries = await Promise.all(
|
|
5776
|
-
|
|
5777
|
-
|
|
6248
|
+
taskIds.map(async (taskId) => {
|
|
6249
|
+
let raw;
|
|
6250
|
+
try {
|
|
6251
|
+
raw = await store.get(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
6252
|
+
} catch {
|
|
6253
|
+
return null;
|
|
6254
|
+
}
|
|
5778
6255
|
if (!raw) return null;
|
|
6256
|
+
if (!await callerOwnsTask(ctx, taskId, runtime?.env.store)) {
|
|
6257
|
+
return null;
|
|
6258
|
+
}
|
|
5779
6259
|
try {
|
|
5780
6260
|
const parsed = JSON.parse(raw);
|
|
5781
|
-
|
|
5782
|
-
if (!await callerOwnsTask(ctx, taskId)) return null;
|
|
5783
|
-
return {
|
|
5784
|
-
...parsed,
|
|
5785
|
-
expires_at: computeExpiresAt(parsed.asked_at)
|
|
5786
|
-
};
|
|
6261
|
+
return { ...parsed, expires_at: computeExpiresAt(parsed.asked_at) };
|
|
5787
6262
|
} catch {
|
|
5788
6263
|
return null;
|
|
5789
6264
|
}
|
|
5790
6265
|
})
|
|
5791
6266
|
);
|
|
5792
|
-
const validEntries = entries.filter(
|
|
6267
|
+
const validEntries = entries.filter(
|
|
6268
|
+
(e) => e !== null
|
|
6269
|
+
);
|
|
5793
6270
|
return {
|
|
5794
6271
|
content: [
|
|
5795
6272
|
{
|
|
5796
6273
|
type: "text",
|
|
5797
|
-
text: JSON.stringify(
|
|
5798
|
-
pending_tasks: validEntries,
|
|
5799
|
-
|
|
5800
|
-
|
|
6274
|
+
text: JSON.stringify(
|
|
6275
|
+
{ pending_tasks: validEntries, count: validEntries.length },
|
|
6276
|
+
null,
|
|
6277
|
+
2
|
|
6278
|
+
)
|
|
5801
6279
|
}
|
|
5802
6280
|
]
|
|
5803
6281
|
};
|
|
@@ -5807,277 +6285,6 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5807
6285
|
|
|
5808
6286
|
// src/tools-ui-agent-schedule.ts
|
|
5809
6287
|
import { z as z6 } from "zod";
|
|
5810
|
-
|
|
5811
|
-
// src/manus-schedule.ts
|
|
5812
|
-
var ManusScheduleError = class extends Error {
|
|
5813
|
-
constructor(message, statusCode) {
|
|
5814
|
-
super(message);
|
|
5815
|
-
this.statusCode = statusCode;
|
|
5816
|
-
this.name = "ManusScheduleError";
|
|
5817
|
-
}
|
|
5818
|
-
statusCode;
|
|
5819
|
-
};
|
|
5820
|
-
function expandCronMinuteField(minuteField) {
|
|
5821
|
-
if (minuteField === "*" || minuteField.includes(" ")) {
|
|
5822
|
-
return null;
|
|
5823
|
-
}
|
|
5824
|
-
const tokens = minuteField.split(",").map((t) => t.trim()).filter(Boolean);
|
|
5825
|
-
const set = /* @__PURE__ */ new Set();
|
|
5826
|
-
for (const token2 of tokens) {
|
|
5827
|
-
const stepWildcard = /^[*]\/(\d+)$/.exec(token2);
|
|
5828
|
-
if (stepWildcard) {
|
|
5829
|
-
const step = parseInt(stepWildcard[1] ?? "0", 10);
|
|
5830
|
-
if (step < 1) return null;
|
|
5831
|
-
for (let m = 0; m < 60; m += step) set.add(m);
|
|
5832
|
-
continue;
|
|
5833
|
-
}
|
|
5834
|
-
const rangeWithStep = /^(\d+)-(\d+)\/(\d+)$/.exec(token2);
|
|
5835
|
-
if (rangeWithStep) {
|
|
5836
|
-
const start = parseInt(rangeWithStep[1] ?? "0", 10);
|
|
5837
|
-
const end = parseInt(rangeWithStep[2] ?? "0", 10);
|
|
5838
|
-
const step = parseInt(rangeWithStep[3] ?? "0", 10);
|
|
5839
|
-
if (step < 1 || start > end) return null;
|
|
5840
|
-
for (let m = start; m <= end; m += step) set.add(m);
|
|
5841
|
-
continue;
|
|
5842
|
-
}
|
|
5843
|
-
const rangeOnly = /^(\d+)-(\d+)$/.exec(token2);
|
|
5844
|
-
if (rangeOnly) {
|
|
5845
|
-
const start = parseInt(rangeOnly[1] ?? "0", 10);
|
|
5846
|
-
const end = parseInt(rangeOnly[2] ?? "0", 10);
|
|
5847
|
-
if (start > end) return null;
|
|
5848
|
-
for (let m = start; m <= end; m++) set.add(m);
|
|
5849
|
-
continue;
|
|
5850
|
-
}
|
|
5851
|
-
const single = /^(\d+)$/.exec(token2);
|
|
5852
|
-
if (single) {
|
|
5853
|
-
set.add(parseInt(single[1] ?? "0", 10));
|
|
5854
|
-
continue;
|
|
5855
|
-
}
|
|
5856
|
-
return null;
|
|
5857
|
-
}
|
|
5858
|
-
const arr = Array.from(set).sort((a, b) => a - b);
|
|
5859
|
-
for (const v of arr) {
|
|
5860
|
-
if (v < 0 || v > 59) return null;
|
|
5861
|
-
}
|
|
5862
|
-
return arr;
|
|
5863
|
-
}
|
|
5864
|
-
function minuteFieldViolatesFiveMinuteRule(minuteField, fullCron) {
|
|
5865
|
-
const minutes = expandCronMinuteField(minuteField);
|
|
5866
|
-
if (minutes === null) {
|
|
5867
|
-
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.`;
|
|
5868
|
-
}
|
|
5869
|
-
if (minutes.length === 0) {
|
|
5870
|
-
return `Cron expression rejected: minute field '${minuteField}' expands to no valid minutes.`;
|
|
5871
|
-
}
|
|
5872
|
-
if (minutes.length === 1) {
|
|
5873
|
-
return null;
|
|
5874
|
-
}
|
|
5875
|
-
for (let i = 1; i < minutes.length; i++) {
|
|
5876
|
-
const gap = (minutes[i] ?? 0) - (minutes[i - 1] ?? 0);
|
|
5877
|
-
if (gap < 5) {
|
|
5878
|
-
return `Cron expression rejected: '${fullCron}' implies a ${gap}-minute gap in the minute field. Minimum allowed interval is 5 minutes.`;
|
|
5879
|
-
}
|
|
5880
|
-
}
|
|
5881
|
-
const wrapGap = 60 - (minutes[minutes.length - 1] ?? 0) + (minutes[0] ?? 0);
|
|
5882
|
-
if (wrapGap < 5) {
|
|
5883
|
-
return `Cron expression rejected: '${fullCron}' implies a ${wrapGap}-minute wraparound gap in the minute field. Minimum allowed interval is 5 minutes.`;
|
|
5884
|
-
}
|
|
5885
|
-
return null;
|
|
5886
|
-
}
|
|
5887
|
-
function validateCron(cron) {
|
|
5888
|
-
const parts = cron.trim().split(/\s+/);
|
|
5889
|
-
if (parts.length !== 5) {
|
|
5890
|
-
return `Invalid cron expression: expected 5 fields (minute hour day month weekday), got ${parts.length}.`;
|
|
5891
|
-
}
|
|
5892
|
-
const [minuteField] = parts;
|
|
5893
|
-
if (minuteField === "*") {
|
|
5894
|
-
return "Cron expression rejected: '* * * * *' runs every minute. Minimum allowed interval is 5 minutes.";
|
|
5895
|
-
}
|
|
5896
|
-
return minuteFieldViolatesFiveMinuteRule(minuteField ?? "", cron);
|
|
5897
|
-
}
|
|
5898
|
-
async function createSchedule(apiKey, params) {
|
|
5899
|
-
const body2 = {
|
|
5900
|
-
name: params.name,
|
|
5901
|
-
cron: params.cron,
|
|
5902
|
-
prompt_template: params.prompt_template,
|
|
5903
|
-
agent_profile: params.agent_profile ?? "manus-1.6-lite",
|
|
5904
|
-
interactive_mode: false,
|
|
5905
|
-
hide_in_task_list: false
|
|
5906
|
-
};
|
|
5907
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.create`, {
|
|
5908
|
-
method: "POST",
|
|
5909
|
-
headers: {
|
|
5910
|
-
"x-manus-api-key": apiKey,
|
|
5911
|
-
"Content-Type": "application/json"
|
|
5912
|
-
},
|
|
5913
|
-
body: JSON.stringify(body2)
|
|
5914
|
-
});
|
|
5915
|
-
if (!res.ok) {
|
|
5916
|
-
throw new ManusScheduleError(
|
|
5917
|
-
`schedule.create failed: HTTP ${res.status}`,
|
|
5918
|
-
res.status
|
|
5919
|
-
);
|
|
5920
|
-
}
|
|
5921
|
-
return await res.json();
|
|
5922
|
-
}
|
|
5923
|
-
async function listSchedules(apiKey) {
|
|
5924
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.list`, {
|
|
5925
|
-
headers: { "x-manus-api-key": apiKey }
|
|
5926
|
-
});
|
|
5927
|
-
if (!res.ok) {
|
|
5928
|
-
throw new ManusScheduleError(
|
|
5929
|
-
`schedule.list failed: HTTP ${res.status}`,
|
|
5930
|
-
res.status
|
|
5931
|
-
);
|
|
5932
|
-
}
|
|
5933
|
-
const data = await res.json();
|
|
5934
|
-
if (Array.isArray(data)) {
|
|
5935
|
-
return { ok: true, schedules: data };
|
|
5936
|
-
}
|
|
5937
|
-
return data;
|
|
5938
|
-
}
|
|
5939
|
-
async function deleteSchedule(apiKey, scheduleId) {
|
|
5940
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.delete`, {
|
|
5941
|
-
method: "POST",
|
|
5942
|
-
headers: {
|
|
5943
|
-
"x-manus-api-key": apiKey,
|
|
5944
|
-
"Content-Type": "application/json"
|
|
5945
|
-
},
|
|
5946
|
-
body: JSON.stringify({ schedule_id: scheduleId })
|
|
5947
|
-
});
|
|
5948
|
-
if (!res.ok) {
|
|
5949
|
-
throw new ManusScheduleError(
|
|
5950
|
-
`schedule.delete failed: HTTP ${res.status}`,
|
|
5951
|
-
res.status
|
|
5952
|
-
);
|
|
5953
|
-
}
|
|
5954
|
-
return await res.json();
|
|
5955
|
-
}
|
|
5956
|
-
async function pauseSchedule(apiKey, scheduleId) {
|
|
5957
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.pause`, {
|
|
5958
|
-
method: "POST",
|
|
5959
|
-
headers: {
|
|
5960
|
-
"x-manus-api-key": apiKey,
|
|
5961
|
-
"Content-Type": "application/json"
|
|
5962
|
-
},
|
|
5963
|
-
body: JSON.stringify({ schedule_id: scheduleId })
|
|
5964
|
-
});
|
|
5965
|
-
if (!res.ok) {
|
|
5966
|
-
throw new ManusScheduleError(
|
|
5967
|
-
`schedule.pause failed: HTTP ${res.status}`,
|
|
5968
|
-
res.status
|
|
5969
|
-
);
|
|
5970
|
-
}
|
|
5971
|
-
return await res.json();
|
|
5972
|
-
}
|
|
5973
|
-
async function resumeSchedule(apiKey, scheduleId) {
|
|
5974
|
-
const res = await fetch(`${MANUS_API_BASE}/schedule.resume`, {
|
|
5975
|
-
method: "POST",
|
|
5976
|
-
headers: {
|
|
5977
|
-
"x-manus-api-key": apiKey,
|
|
5978
|
-
"Content-Type": "application/json"
|
|
5979
|
-
},
|
|
5980
|
-
body: JSON.stringify({ schedule_id: scheduleId })
|
|
5981
|
-
});
|
|
5982
|
-
if (!res.ok) {
|
|
5983
|
-
throw new ManusScheduleError(
|
|
5984
|
-
`schedule.resume failed: HTTP ${res.status}`,
|
|
5985
|
-
res.status
|
|
5986
|
-
);
|
|
5987
|
-
}
|
|
5988
|
-
return await res.json();
|
|
5989
|
-
}
|
|
5990
|
-
|
|
5991
|
-
// src/manus-usage.ts
|
|
5992
|
-
var ManusUsageError = class extends Error {
|
|
5993
|
-
constructor(message, statusCode) {
|
|
5994
|
-
super(message);
|
|
5995
|
-
this.statusCode = statusCode;
|
|
5996
|
-
this.name = "ManusUsageError";
|
|
5997
|
-
}
|
|
5998
|
-
statusCode;
|
|
5999
|
-
};
|
|
6000
|
-
var CACHE_KEY = "manus_usage_cache";
|
|
6001
|
-
var CACHE_TTL_SECONDS = 60;
|
|
6002
|
-
var USAGE_THRESHOLD_WARNING = 1e3;
|
|
6003
|
-
var USAGE_THRESHOLD_CRITICAL = 100;
|
|
6004
|
-
async function fetchUsageFromApi(apiKey) {
|
|
6005
|
-
const now = /* @__PURE__ */ new Date();
|
|
6006
|
-
const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
6007
|
-
const [usageRes, creditsRes] = await Promise.all([
|
|
6008
|
-
fetch(`${MANUS_API_BASE}/usage.get`, {
|
|
6009
|
-
headers: { "x-manus-api-key": apiKey }
|
|
6010
|
-
}),
|
|
6011
|
-
fetch(`${MANUS_API_BASE}/credits.get`, {
|
|
6012
|
-
headers: { "x-manus-api-key": apiKey }
|
|
6013
|
-
})
|
|
6014
|
-
]);
|
|
6015
|
-
if (!usageRes.ok && !creditsRes.ok) {
|
|
6016
|
-
throw new ManusUsageError(
|
|
6017
|
-
`Manus usage APIs failed: usage.get HTTP ${usageRes.status}, credits.get HTTP ${creditsRes.status}`,
|
|
6018
|
-
Math.max(usageRes.status, creditsRes.status)
|
|
6019
|
-
);
|
|
6020
|
-
}
|
|
6021
|
-
let spentCredits = 0;
|
|
6022
|
-
let remainingCredits = 0;
|
|
6023
|
-
let taskCount = 0;
|
|
6024
|
-
if (usageRes.ok) {
|
|
6025
|
-
const usageData = await usageRes.json();
|
|
6026
|
-
spentCredits = usageData.spent_credits ?? usageData.credits_used ?? 0;
|
|
6027
|
-
taskCount = usageData.task_count ?? usageData.tasks_run ?? 0;
|
|
6028
|
-
if (usageData.remaining_credits !== void 0 || usageData.credits_remaining !== void 0) {
|
|
6029
|
-
remainingCredits = usageData.remaining_credits ?? usageData.credits_remaining ?? 0;
|
|
6030
|
-
}
|
|
6031
|
-
}
|
|
6032
|
-
if (creditsRes.ok) {
|
|
6033
|
-
const creditsData = await creditsRes.json();
|
|
6034
|
-
const fromCreditsApi = creditsData.remaining_credits ?? creditsData.credits_remaining ?? creditsData.balance;
|
|
6035
|
-
if (fromCreditsApi !== void 0) {
|
|
6036
|
-
remainingCredits = fromCreditsApi;
|
|
6037
|
-
}
|
|
6038
|
-
}
|
|
6039
|
-
return { month, spent_credits: spentCredits, remaining_credits: remainingCredits, task_count: taskCount };
|
|
6040
|
-
}
|
|
6041
|
-
async function getUsage(apiKey, env) {
|
|
6042
|
-
const cached = await env.CARRIER_USERS.get(CACHE_KEY, "json");
|
|
6043
|
-
const nowMs = Date.now();
|
|
6044
|
-
if (cached && nowMs - cached.fetched_at < CACHE_TTL_SECONDS * 1e3) {
|
|
6045
|
-
return { data: cached.data, from_cache: true };
|
|
6046
|
-
}
|
|
6047
|
-
const data = await fetchUsageFromApi(apiKey);
|
|
6048
|
-
await env.CARRIER_USERS.put(
|
|
6049
|
-
CACHE_KEY,
|
|
6050
|
-
JSON.stringify({ data, fetched_at: nowMs }),
|
|
6051
|
-
{ expirationTtl: CACHE_TTL_SECONDS }
|
|
6052
|
-
);
|
|
6053
|
-
return { data, from_cache: false };
|
|
6054
|
-
}
|
|
6055
|
-
function emitUsageThresholdIfNeeded(env, data) {
|
|
6056
|
-
const { remaining_credits, month, task_count } = data;
|
|
6057
|
-
if (remaining_credits < USAGE_THRESHOLD_CRITICAL) {
|
|
6058
|
-
writeUsageThresholdAudit(env, "critical", remaining_credits, month, task_count);
|
|
6059
|
-
} else if (remaining_credits < USAGE_THRESHOLD_WARNING) {
|
|
6060
|
-
writeUsageThresholdAudit(env, "warning", remaining_credits, month, task_count);
|
|
6061
|
-
}
|
|
6062
|
-
}
|
|
6063
|
-
function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCount) {
|
|
6064
|
-
try {
|
|
6065
|
-
env.AUDIT_LOG.writeDataPoint({
|
|
6066
|
-
blobs: [
|
|
6067
|
-
"manus_usage_threshold",
|
|
6068
|
-
severity,
|
|
6069
|
-
month,
|
|
6070
|
-
String(taskCount)
|
|
6071
|
-
],
|
|
6072
|
-
doubles: [remainingCredits],
|
|
6073
|
-
indexes: ["manus_usage"]
|
|
6074
|
-
});
|
|
6075
|
-
} catch (err7) {
|
|
6076
|
-
console.error(`[manus-usage] threshold audit write failed: ${err7.message}`);
|
|
6077
|
-
}
|
|
6078
|
-
}
|
|
6079
|
-
|
|
6080
|
-
// src/tools-ui-agent-schedule.ts
|
|
6081
6288
|
var UI_AGENT_SCHEDULE_TOOL_SCOPES = {
|
|
6082
6289
|
ui_agent_schedule_create: "admin",
|
|
6083
6290
|
ui_agent_schedule_list: "read",
|
|
@@ -6086,20 +6293,7 @@ var UI_AGENT_SCHEDULE_TOOL_SCOPES = {
|
|
|
6086
6293
|
ui_agent_schedule_resume: "admin",
|
|
6087
6294
|
ui_agent_usage: "read"
|
|
6088
6295
|
};
|
|
6089
|
-
|
|
6090
|
-
return {
|
|
6091
|
-
isError: true,
|
|
6092
|
-
content: [
|
|
6093
|
-
{
|
|
6094
|
-
type: "text",
|
|
6095
|
-
text: JSON.stringify({
|
|
6096
|
-
error: "manus_api_not_configured",
|
|
6097
|
-
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."
|
|
6098
|
-
})
|
|
6099
|
-
}
|
|
6100
|
-
]
|
|
6101
|
-
};
|
|
6102
|
-
}
|
|
6296
|
+
var STDIO_NOTE = " Requires the remote Carrier MCP Worker deployment \u2014 a local CLI has no recurring runtime, so schedules cannot fire here.";
|
|
6103
6297
|
function scopeError(toolName, required, actual) {
|
|
6104
6298
|
return {
|
|
6105
6299
|
isError: true,
|
|
@@ -6111,427 +6305,126 @@ function scopeError(toolName, required, actual) {
|
|
|
6111
6305
|
]
|
|
6112
6306
|
};
|
|
6113
6307
|
}
|
|
6114
|
-
function
|
|
6308
|
+
function scopeDenied(ctx, toolName, required) {
|
|
6309
|
+
ctx.audit({
|
|
6310
|
+
tool_name: toolName,
|
|
6311
|
+
ocs_method: `[ui-agent:${toolName}]`,
|
|
6312
|
+
status: "scope_denied",
|
|
6313
|
+
dry_run: false,
|
|
6314
|
+
duration_ms: 0
|
|
6315
|
+
});
|
|
6316
|
+
return scopeError(toolName, required, ctx.props.scope);
|
|
6317
|
+
}
|
|
6318
|
+
function registerScheduleAndUsageTools(server2, ctx, runtime) {
|
|
6115
6319
|
server2.registerTool(
|
|
6116
6320
|
"ui_agent_schedule_create",
|
|
6117
6321
|
{
|
|
6118
|
-
title: "Create
|
|
6119
|
-
description: "Creates a recurring
|
|
6322
|
+
title: "Create UI Agent Schedule",
|
|
6323
|
+
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,
|
|
6120
6324
|
inputSchema: {
|
|
6121
6325
|
name: z6.string().describe("Human-readable name for this schedule"),
|
|
6122
6326
|
cron: z6.string().describe(
|
|
6123
6327
|
"Standard 5-field cron expression (minute hour day month weekday). Minimum interval: 5 minutes. Example: '0 */6 * * *' = every 6 hours."
|
|
6124
6328
|
),
|
|
6125
|
-
prompt_template: z6.string().describe(
|
|
6126
|
-
|
|
6329
|
+
prompt_template: z6.string().describe(
|
|
6330
|
+
"Agent prompt/task template the browser agent will execute on each run"
|
|
6331
|
+
),
|
|
6332
|
+
profile: z6.string().optional().describe(
|
|
6333
|
+
"Browser-agent profile to use. Defaults to the deployment's configured profile."
|
|
6334
|
+
)
|
|
6127
6335
|
}
|
|
6128
6336
|
},
|
|
6129
|
-
async (
|
|
6130
|
-
const start = Date.now();
|
|
6131
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6132
|
-
ctx.audit({
|
|
6133
|
-
tool_name: "ui_agent_schedule_create",
|
|
6134
|
-
ocs_method: "[manus:schedule.create]",
|
|
6135
|
-
status: "scope_denied",
|
|
6136
|
-
dry_run: false,
|
|
6137
|
-
duration_ms: 0
|
|
6138
|
-
});
|
|
6139
|
-
return scopeError("ui_agent_schedule_create", "admin", ctx.props.scope);
|
|
6140
|
-
}
|
|
6141
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6142
|
-
return noManusKeyError2();
|
|
6143
|
-
}
|
|
6144
|
-
const cronError = validateCron(args.cron);
|
|
6145
|
-
if (cronError) {
|
|
6146
|
-
return {
|
|
6147
|
-
isError: true,
|
|
6148
|
-
content: [{ type: "text", text: JSON.stringify({ error: "invalid_cron", message: cronError }) }]
|
|
6149
|
-
};
|
|
6150
|
-
}
|
|
6151
|
-
try {
|
|
6152
|
-
const result2 = await createSchedule(ctx.env.MANUS_API_KEY, {
|
|
6153
|
-
name: args.name,
|
|
6154
|
-
cron: args.cron,
|
|
6155
|
-
prompt_template: args.prompt_template,
|
|
6156
|
-
agent_profile: args.profile
|
|
6157
|
-
});
|
|
6158
|
-
ctx.audit({
|
|
6159
|
-
tool_name: "ui_agent_schedule_create",
|
|
6160
|
-
ocs_method: "[manus:schedule.create]",
|
|
6161
|
-
status: result2.ok ? "ok" : "error",
|
|
6162
|
-
dry_run: false,
|
|
6163
|
-
duration_ms: Date.now() - start,
|
|
6164
|
-
event_type: "ui_agent_dispatch"
|
|
6165
|
-
});
|
|
6166
|
-
if (!result2.ok || !result2.schedule_id) {
|
|
6167
|
-
return {
|
|
6168
|
-
isError: true,
|
|
6169
|
-
content: [
|
|
6170
|
-
{
|
|
6171
|
-
type: "text",
|
|
6172
|
-
text: JSON.stringify({
|
|
6173
|
-
error: "schedule_create_failed",
|
|
6174
|
-
message: result2.error?.message ?? "Manus schedule.create returned ok=false",
|
|
6175
|
-
code: result2.error?.code
|
|
6176
|
-
})
|
|
6177
|
-
}
|
|
6178
|
-
]
|
|
6179
|
-
};
|
|
6180
|
-
}
|
|
6181
|
-
return {
|
|
6182
|
-
content: [
|
|
6183
|
-
{
|
|
6184
|
-
type: "text",
|
|
6185
|
-
text: JSON.stringify({
|
|
6186
|
-
status: "created",
|
|
6187
|
-
schedule_id: result2.schedule_id,
|
|
6188
|
-
name: args.name,
|
|
6189
|
-
cron: args.cron
|
|
6190
|
-
}, null, 2)
|
|
6191
|
-
}
|
|
6192
|
-
]
|
|
6193
|
-
};
|
|
6194
|
-
} catch (err7) {
|
|
6195
|
-
ctx.audit({
|
|
6196
|
-
tool_name: "ui_agent_schedule_create",
|
|
6197
|
-
ocs_method: "[manus:schedule.create]",
|
|
6198
|
-
status: "error",
|
|
6199
|
-
dry_run: false,
|
|
6200
|
-
duration_ms: Date.now() - start,
|
|
6201
|
-
event_type: "ui_agent_dispatch"
|
|
6202
|
-
});
|
|
6203
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6204
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6205
|
-
}
|
|
6206
|
-
}
|
|
6337
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_create", "admin") : uiAgentStubError("ui_agent_schedule_create", "recurring_runtime")
|
|
6207
6338
|
);
|
|
6208
6339
|
server2.registerTool(
|
|
6209
6340
|
"ui_agent_schedule_list",
|
|
6210
6341
|
{
|
|
6211
|
-
title: "List
|
|
6212
|
-
description: "Lists all
|
|
6342
|
+
title: "List UI Agent Schedules",
|
|
6343
|
+
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,
|
|
6213
6344
|
inputSchema: {}
|
|
6214
6345
|
},
|
|
6215
|
-
async () =>
|
|
6216
|
-
const start = Date.now();
|
|
6217
|
-
if (!ctx.props.scope.includes("read")) {
|
|
6218
|
-
ctx.audit({
|
|
6219
|
-
tool_name: "ui_agent_schedule_list",
|
|
6220
|
-
ocs_method: "[manus:schedule.list]",
|
|
6221
|
-
status: "scope_denied",
|
|
6222
|
-
dry_run: false,
|
|
6223
|
-
duration_ms: 0
|
|
6224
|
-
});
|
|
6225
|
-
return scopeError("ui_agent_schedule_list", "read", ctx.props.scope);
|
|
6226
|
-
}
|
|
6227
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6228
|
-
return noManusKeyError2();
|
|
6229
|
-
}
|
|
6230
|
-
try {
|
|
6231
|
-
const result2 = await listSchedules(ctx.env.MANUS_API_KEY);
|
|
6232
|
-
ctx.audit({
|
|
6233
|
-
tool_name: "ui_agent_schedule_list",
|
|
6234
|
-
ocs_method: "[manus:schedule.list]",
|
|
6235
|
-
status: result2.ok ? "ok" : "error",
|
|
6236
|
-
dry_run: false,
|
|
6237
|
-
duration_ms: Date.now() - start
|
|
6238
|
-
});
|
|
6239
|
-
if (!result2.ok) {
|
|
6240
|
-
return {
|
|
6241
|
-
isError: true,
|
|
6242
|
-
content: [
|
|
6243
|
-
{
|
|
6244
|
-
type: "text",
|
|
6245
|
-
text: JSON.stringify({
|
|
6246
|
-
error: "schedule_list_failed",
|
|
6247
|
-
message: result2.error?.message ?? "Manus schedule.list returned ok=false"
|
|
6248
|
-
})
|
|
6249
|
-
}
|
|
6250
|
-
]
|
|
6251
|
-
};
|
|
6252
|
-
}
|
|
6253
|
-
return {
|
|
6254
|
-
content: [
|
|
6255
|
-
{
|
|
6256
|
-
type: "text",
|
|
6257
|
-
text: JSON.stringify({ schedules: result2.schedules ?? [] }, null, 2)
|
|
6258
|
-
}
|
|
6259
|
-
]
|
|
6260
|
-
};
|
|
6261
|
-
} catch (err7) {
|
|
6262
|
-
ctx.audit({
|
|
6263
|
-
tool_name: "ui_agent_schedule_list",
|
|
6264
|
-
ocs_method: "[manus:schedule.list]",
|
|
6265
|
-
status: "error",
|
|
6266
|
-
dry_run: false,
|
|
6267
|
-
duration_ms: Date.now() - start
|
|
6268
|
-
});
|
|
6269
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6270
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6271
|
-
}
|
|
6272
|
-
}
|
|
6346
|
+
async () => !ctx.props.scope.includes("read") ? scopeDenied(ctx, "ui_agent_schedule_list", "read") : uiAgentStubError("ui_agent_schedule_list", "recurring_runtime")
|
|
6273
6347
|
);
|
|
6274
6348
|
server2.registerTool(
|
|
6275
6349
|
"ui_agent_schedule_delete",
|
|
6276
6350
|
{
|
|
6277
|
-
title: "Delete
|
|
6278
|
-
description: "Permanently deletes a
|
|
6351
|
+
title: "Delete UI Agent Schedule",
|
|
6352
|
+
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,
|
|
6279
6353
|
inputSchema: {
|
|
6280
6354
|
schedule_id: z6.string().describe("ID of the schedule to delete")
|
|
6281
6355
|
}
|
|
6282
6356
|
},
|
|
6283
|
-
async (
|
|
6284
|
-
const start = Date.now();
|
|
6285
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6286
|
-
ctx.audit({
|
|
6287
|
-
tool_name: "ui_agent_schedule_delete",
|
|
6288
|
-
ocs_method: "[manus:schedule.delete]",
|
|
6289
|
-
status: "scope_denied",
|
|
6290
|
-
dry_run: false,
|
|
6291
|
-
duration_ms: 0
|
|
6292
|
-
});
|
|
6293
|
-
return scopeError("ui_agent_schedule_delete", "admin", ctx.props.scope);
|
|
6294
|
-
}
|
|
6295
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6296
|
-
return noManusKeyError2();
|
|
6297
|
-
}
|
|
6298
|
-
try {
|
|
6299
|
-
const result2 = await deleteSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
|
|
6300
|
-
ctx.audit({
|
|
6301
|
-
tool_name: "ui_agent_schedule_delete",
|
|
6302
|
-
ocs_method: "[manus:schedule.delete]",
|
|
6303
|
-
status: result2.ok ? "ok" : "error",
|
|
6304
|
-
dry_run: false,
|
|
6305
|
-
duration_ms: Date.now() - start,
|
|
6306
|
-
event_type: "ui_agent_dispatch"
|
|
6307
|
-
});
|
|
6308
|
-
if (!result2.ok) {
|
|
6309
|
-
return {
|
|
6310
|
-
isError: true,
|
|
6311
|
-
content: [
|
|
6312
|
-
{
|
|
6313
|
-
type: "text",
|
|
6314
|
-
text: JSON.stringify({
|
|
6315
|
-
error: "schedule_delete_failed",
|
|
6316
|
-
message: result2.error?.message ?? "Manus schedule.delete returned ok=false"
|
|
6317
|
-
})
|
|
6318
|
-
}
|
|
6319
|
-
]
|
|
6320
|
-
};
|
|
6321
|
-
}
|
|
6322
|
-
return {
|
|
6323
|
-
content: [
|
|
6324
|
-
{
|
|
6325
|
-
type: "text",
|
|
6326
|
-
text: JSON.stringify({ status: "deleted", schedule_id: args.schedule_id }, null, 2)
|
|
6327
|
-
}
|
|
6328
|
-
]
|
|
6329
|
-
};
|
|
6330
|
-
} catch (err7) {
|
|
6331
|
-
ctx.audit({
|
|
6332
|
-
tool_name: "ui_agent_schedule_delete",
|
|
6333
|
-
ocs_method: "[manus:schedule.delete]",
|
|
6334
|
-
status: "error",
|
|
6335
|
-
dry_run: false,
|
|
6336
|
-
duration_ms: Date.now() - start,
|
|
6337
|
-
event_type: "ui_agent_dispatch"
|
|
6338
|
-
});
|
|
6339
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6340
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6341
|
-
}
|
|
6342
|
-
}
|
|
6357
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_delete", "admin") : uiAgentStubError("ui_agent_schedule_delete", "recurring_runtime")
|
|
6343
6358
|
);
|
|
6344
6359
|
server2.registerTool(
|
|
6345
6360
|
"ui_agent_schedule_pause",
|
|
6346
6361
|
{
|
|
6347
|
-
title: "Pause
|
|
6348
|
-
description: "Pauses an active
|
|
6362
|
+
title: "Pause UI Agent Schedule",
|
|
6363
|
+
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,
|
|
6349
6364
|
inputSchema: {
|
|
6350
|
-
schedule_id: z6.string().describe("ID of the schedule to pause")
|
|
6351
|
-
}
|
|
6352
|
-
},
|
|
6353
|
-
async (args) => {
|
|
6354
|
-
const start = Date.now();
|
|
6355
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6356
|
-
ctx.audit({
|
|
6357
|
-
tool_name: "ui_agent_schedule_pause",
|
|
6358
|
-
ocs_method: "[manus:schedule.pause]",
|
|
6359
|
-
status: "scope_denied",
|
|
6360
|
-
dry_run: false,
|
|
6361
|
-
duration_ms: 0
|
|
6362
|
-
});
|
|
6363
|
-
return scopeError("ui_agent_schedule_pause", "admin", ctx.props.scope);
|
|
6364
|
-
}
|
|
6365
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6366
|
-
return noManusKeyError2();
|
|
6367
|
-
}
|
|
6368
|
-
try {
|
|
6369
|
-
const result2 = await pauseSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
|
|
6370
|
-
ctx.audit({
|
|
6371
|
-
tool_name: "ui_agent_schedule_pause",
|
|
6372
|
-
ocs_method: "[manus:schedule.pause]",
|
|
6373
|
-
status: result2.ok ? "ok" : "error",
|
|
6374
|
-
dry_run: false,
|
|
6375
|
-
duration_ms: Date.now() - start,
|
|
6376
|
-
event_type: "ui_agent_dispatch"
|
|
6377
|
-
});
|
|
6378
|
-
if (!result2.ok) {
|
|
6379
|
-
return {
|
|
6380
|
-
isError: true,
|
|
6381
|
-
content: [
|
|
6382
|
-
{
|
|
6383
|
-
type: "text",
|
|
6384
|
-
text: JSON.stringify({
|
|
6385
|
-
error: "schedule_pause_failed",
|
|
6386
|
-
message: result2.error?.message ?? "Manus schedule.pause returned ok=false"
|
|
6387
|
-
})
|
|
6388
|
-
}
|
|
6389
|
-
]
|
|
6390
|
-
};
|
|
6391
|
-
}
|
|
6392
|
-
return {
|
|
6393
|
-
content: [
|
|
6394
|
-
{
|
|
6395
|
-
type: "text",
|
|
6396
|
-
text: JSON.stringify({ status: "paused", schedule_id: args.schedule_id }, null, 2)
|
|
6397
|
-
}
|
|
6398
|
-
]
|
|
6399
|
-
};
|
|
6400
|
-
} catch (err7) {
|
|
6401
|
-
ctx.audit({
|
|
6402
|
-
tool_name: "ui_agent_schedule_pause",
|
|
6403
|
-
ocs_method: "[manus:schedule.pause]",
|
|
6404
|
-
status: "error",
|
|
6405
|
-
dry_run: false,
|
|
6406
|
-
duration_ms: Date.now() - start,
|
|
6407
|
-
event_type: "ui_agent_dispatch"
|
|
6408
|
-
});
|
|
6409
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6410
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6365
|
+
schedule_id: z6.string().describe("ID of the schedule to pause")
|
|
6411
6366
|
}
|
|
6412
|
-
}
|
|
6367
|
+
},
|
|
6368
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_pause", "admin") : uiAgentStubError("ui_agent_schedule_pause", "recurring_runtime")
|
|
6413
6369
|
);
|
|
6414
6370
|
server2.registerTool(
|
|
6415
6371
|
"ui_agent_schedule_resume",
|
|
6416
6372
|
{
|
|
6417
|
-
title: "Resume
|
|
6418
|
-
description: "Resumes a paused
|
|
6373
|
+
title: "Resume UI Agent Schedule",
|
|
6374
|
+
description: "Resumes a paused recurring browser-agent schedule. Requires admin scope." + STDIO_NOTE,
|
|
6419
6375
|
inputSchema: {
|
|
6420
6376
|
schedule_id: z6.string().describe("ID of the schedule to resume")
|
|
6421
6377
|
}
|
|
6422
6378
|
},
|
|
6423
|
-
async (
|
|
6424
|
-
const start = Date.now();
|
|
6425
|
-
if (!ctx.props.scope.includes("admin")) {
|
|
6426
|
-
ctx.audit({
|
|
6427
|
-
tool_name: "ui_agent_schedule_resume",
|
|
6428
|
-
ocs_method: "[manus:schedule.resume]",
|
|
6429
|
-
status: "scope_denied",
|
|
6430
|
-
dry_run: false,
|
|
6431
|
-
duration_ms: 0
|
|
6432
|
-
});
|
|
6433
|
-
return scopeError("ui_agent_schedule_resume", "admin", ctx.props.scope);
|
|
6434
|
-
}
|
|
6435
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6436
|
-
return noManusKeyError2();
|
|
6437
|
-
}
|
|
6438
|
-
try {
|
|
6439
|
-
const result2 = await resumeSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
|
|
6440
|
-
ctx.audit({
|
|
6441
|
-
tool_name: "ui_agent_schedule_resume",
|
|
6442
|
-
ocs_method: "[manus:schedule.resume]",
|
|
6443
|
-
status: result2.ok ? "ok" : "error",
|
|
6444
|
-
dry_run: false,
|
|
6445
|
-
duration_ms: Date.now() - start,
|
|
6446
|
-
event_type: "ui_agent_dispatch"
|
|
6447
|
-
});
|
|
6448
|
-
if (!result2.ok) {
|
|
6449
|
-
return {
|
|
6450
|
-
isError: true,
|
|
6451
|
-
content: [
|
|
6452
|
-
{
|
|
6453
|
-
type: "text",
|
|
6454
|
-
text: JSON.stringify({
|
|
6455
|
-
error: "schedule_resume_failed",
|
|
6456
|
-
message: result2.error?.message ?? "Manus schedule.resume returned ok=false"
|
|
6457
|
-
})
|
|
6458
|
-
}
|
|
6459
|
-
]
|
|
6460
|
-
};
|
|
6461
|
-
}
|
|
6462
|
-
return {
|
|
6463
|
-
content: [
|
|
6464
|
-
{
|
|
6465
|
-
type: "text",
|
|
6466
|
-
text: JSON.stringify({ status: "active", schedule_id: args.schedule_id }, null, 2)
|
|
6467
|
-
}
|
|
6468
|
-
]
|
|
6469
|
-
};
|
|
6470
|
-
} catch (err7) {
|
|
6471
|
-
ctx.audit({
|
|
6472
|
-
tool_name: "ui_agent_schedule_resume",
|
|
6473
|
-
ocs_method: "[manus:schedule.resume]",
|
|
6474
|
-
status: "error",
|
|
6475
|
-
dry_run: false,
|
|
6476
|
-
duration_ms: Date.now() - start,
|
|
6477
|
-
event_type: "ui_agent_dispatch"
|
|
6478
|
-
});
|
|
6479
|
-
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
6480
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6481
|
-
}
|
|
6482
|
-
}
|
|
6379
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_resume", "admin") : uiAgentStubError("ui_agent_schedule_resume", "recurring_runtime")
|
|
6483
6380
|
);
|
|
6484
6381
|
server2.registerTool(
|
|
6485
6382
|
"ui_agent_usage",
|
|
6486
6383
|
{
|
|
6487
|
-
title: "
|
|
6488
|
-
description: "
|
|
6384
|
+
title: "UI Agent Usage & Spend",
|
|
6385
|
+
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.",
|
|
6489
6386
|
inputSchema: {}
|
|
6490
6387
|
},
|
|
6491
6388
|
async () => {
|
|
6492
|
-
const start = Date.now();
|
|
6493
6389
|
if (!ctx.props.scope.includes("read")) {
|
|
6494
6390
|
ctx.audit({
|
|
6495
6391
|
tool_name: "ui_agent_usage",
|
|
6496
|
-
ocs_method: "[
|
|
6392
|
+
ocs_method: "[ui-agent:usage]",
|
|
6497
6393
|
status: "scope_denied",
|
|
6498
6394
|
dry_run: false,
|
|
6499
6395
|
duration_ms: 0
|
|
6500
6396
|
});
|
|
6501
|
-
return scopeError("ui_agent_usage", "read", ctx.props.scope);
|
|
6502
|
-
}
|
|
6503
|
-
if (!ctx.env.MANUS_API_KEY) {
|
|
6504
|
-
return noManusKeyError2();
|
|
6505
|
-
}
|
|
6506
|
-
try {
|
|
6507
|
-
const { data, from_cache } = await getUsage(ctx.env.MANUS_API_KEY, ctx.env);
|
|
6508
|
-
emitUsageThresholdIfNeeded(ctx.env, data);
|
|
6509
|
-
ctx.audit({
|
|
6510
|
-
tool_name: "ui_agent_usage",
|
|
6511
|
-
ocs_method: "[manus:usage.get]",
|
|
6512
|
-
status: "ok",
|
|
6513
|
-
dry_run: false,
|
|
6514
|
-
duration_ms: Date.now() - start
|
|
6515
|
-
});
|
|
6516
6397
|
return {
|
|
6398
|
+
isError: true,
|
|
6517
6399
|
content: [
|
|
6518
6400
|
{
|
|
6519
6401
|
type: "text",
|
|
6520
|
-
text:
|
|
6402
|
+
text: `Scope denied: tool 'ui_agent_usage' requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
6521
6403
|
}
|
|
6522
6404
|
]
|
|
6523
6405
|
};
|
|
6524
|
-
} catch (err7) {
|
|
6525
|
-
ctx.audit({
|
|
6526
|
-
tool_name: "ui_agent_usage",
|
|
6527
|
-
ocs_method: "[manus:usage.get]",
|
|
6528
|
-
status: "error",
|
|
6529
|
-
dry_run: false,
|
|
6530
|
-
duration_ms: Date.now() - start
|
|
6531
|
-
});
|
|
6532
|
-
const msg = err7 instanceof Error ? err7.message : String(err7);
|
|
6533
|
-
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
6534
6406
|
}
|
|
6407
|
+
return {
|
|
6408
|
+
content: [
|
|
6409
|
+
{
|
|
6410
|
+
type: "text",
|
|
6411
|
+
text: JSON.stringify(
|
|
6412
|
+
{
|
|
6413
|
+
scope: "cli_session",
|
|
6414
|
+
configured: runtime.configured,
|
|
6415
|
+
runs_billed_this_session: runtime.spend.runs,
|
|
6416
|
+
cost_usd_this_session: runtime.spend.costUsd,
|
|
6417
|
+
runs_dispatched_this_session: runtime.budget.spent,
|
|
6418
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
6419
|
+
run_limit_env: MAX_RUNS_ENV,
|
|
6420
|
+
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."
|
|
6421
|
+
},
|
|
6422
|
+
null,
|
|
6423
|
+
2
|
|
6424
|
+
)
|
|
6425
|
+
}
|
|
6426
|
+
]
|
|
6427
|
+
};
|
|
6535
6428
|
}
|
|
6536
6429
|
);
|
|
6537
6430
|
}
|
|
@@ -6826,6 +6719,11 @@ async function createTopupCheckout(env, args) {
|
|
|
6826
6719
|
const successUrl = args.successUrl ?? env.WALLET_CHECKOUT_SUCCESS_URL ?? DEFAULT_SUCCESS_URL;
|
|
6827
6720
|
const cancelUrl = args.cancelUrl ?? env.WALLET_CHECKOUT_CANCEL_URL ?? DEFAULT_CANCEL_URL;
|
|
6828
6721
|
const bonusLabel = bonusCents > 0 ? ` (+\u20AC${(bonusCents / 100).toFixed(2)} bonus)` : "";
|
|
6722
|
+
if (!env.CARRIER_USERS) {
|
|
6723
|
+
throw new Error(
|
|
6724
|
+
"Wallet top-up is not available in local stdio mode: the prepaid wallet is credited by the hosted Carrier service after payment, and this process cannot reach the customer record that crediting depends on. Creating the session here would bill the card against a duplicate Stripe customer and the wallet would never be credited. Top up from the console at https://app.carrier.llc/billing, or connect to the hosted MCP at https://mcp.carrier.llc/mcp, which has the binding. `wallet_balance` still works here if ATLAS_BASE_URL and CARRIER_INTERNAL_API_KEY are set."
|
|
6725
|
+
);
|
|
6726
|
+
}
|
|
6829
6727
|
const billingSub = await resolveBillingSub(env, args.orgId);
|
|
6830
6728
|
const existingCustomer = await env.CARRIER_USERS.get(`stripe_customer_id:${billingSub}`).catch(() => null);
|
|
6831
6729
|
const params = new URLSearchParams({
|
|
@@ -6875,7 +6773,7 @@ async function runAutoTopup(env, args) {
|
|
|
6875
6773
|
let packCents = args.packCents;
|
|
6876
6774
|
try {
|
|
6877
6775
|
const wallet = await getWallet(env, args.orgId);
|
|
6878
|
-
if (packCents === void 0) packCents = wallet.auto_topup_pack_cents;
|
|
6776
|
+
if (packCents === void 0) packCents = wallet.auto_topup_pack_cents ?? void 0;
|
|
6879
6777
|
} catch (e) {
|
|
6880
6778
|
if (e instanceof WalletClientError) {
|
|
6881
6779
|
return { charged: false, creditedCents: 0, reason: `wallet_unreachable: ${e.message}` };
|
|
@@ -6981,15 +6879,23 @@ function registerWalletTools(server2, ctx) {
|
|
|
6981
6879
|
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`.",
|
|
6982
6880
|
inputSchema: {},
|
|
6983
6881
|
// Derived from the object literal this handler builds below, not from the
|
|
6984
|
-
// description.
|
|
6985
|
-
//
|
|
6986
|
-
//
|
|
6882
|
+
// description. All five keys are always present on the success path, so
|
|
6883
|
+
// none is optional — but the two auto-top-up amounts are nullable, not
|
|
6884
|
+
// just unset. ATLAS types them `number | null` and synthesises an
|
|
6885
|
+
// all-null row for an org with no wallet yet (its toWalletView), so an
|
|
6886
|
+
// org that has never configured auto-top-up — every newly onboarded one
|
|
6887
|
+
// — returns null here. Requiring a number turned that, the most common
|
|
6888
|
+
// state a new operator is in, into an output validation error.
|
|
6987
6889
|
outputSchema: {
|
|
6988
6890
|
org_id: z7.string().describe("Carrier organisation the wallet belongs to"),
|
|
6989
6891
|
balance_eur_cents: z7.number().int().describe("Current balance in EUR cents"),
|
|
6990
6892
|
auto_topup_enabled: z7.boolean(),
|
|
6991
|
-
auto_topup_threshold_cents: z7.number().int().describe(
|
|
6992
|
-
|
|
6893
|
+
auto_topup_threshold_cents: z7.number().int().nullable().describe(
|
|
6894
|
+
"Balance at or below which an auto top-up fires, EUR cents. Null when auto-top-up is unconfigured."
|
|
6895
|
+
),
|
|
6896
|
+
auto_topup_pack_cents: z7.number().int().nullable().describe(
|
|
6897
|
+
"Amount an auto top-up charges, EUR cents. Null when auto-top-up is unconfigured."
|
|
6898
|
+
)
|
|
6993
6899
|
},
|
|
6994
6900
|
annotations: annotationsFor("wallet_balance", "read")
|
|
6995
6901
|
},
|
|
@@ -7181,7 +7087,39 @@ function registerWalletTools(server2, ctx) {
|
|
|
7181
7087
|
// src/stripe-connect-tools.ts
|
|
7182
7088
|
import { z as z8 } from "zod";
|
|
7183
7089
|
import * as Sentry3 from "@sentry/cloudflare";
|
|
7090
|
+
|
|
7091
|
+
// src/binding-unavailable.ts
|
|
7092
|
+
var HOSTED_MCP_URL = "https://mcp.carrier.llc/mcp";
|
|
7093
|
+
var BindingUnavailableError = class extends Error {
|
|
7094
|
+
constructor(tool, binding, message) {
|
|
7095
|
+
super(message);
|
|
7096
|
+
this.tool = tool;
|
|
7097
|
+
this.binding = binding;
|
|
7098
|
+
this.name = "BindingUnavailableError";
|
|
7099
|
+
}
|
|
7100
|
+
tool;
|
|
7101
|
+
binding;
|
|
7102
|
+
};
|
|
7103
|
+
function requireUserStore(env, tool, subject) {
|
|
7104
|
+
requireBinding(env.CARRIER_USERS, tool, "CARRIER_USERS", subject);
|
|
7105
|
+
}
|
|
7106
|
+
function requireBinding(binding, tool, name, subject) {
|
|
7107
|
+
if (binding) return;
|
|
7108
|
+
throw new BindingUnavailableError(
|
|
7109
|
+
tool,
|
|
7110
|
+
name,
|
|
7111
|
+
`${tool} cannot answer here: ${subject} is held by the hosted Carrier service, and this local MCP server has no connection to it. There is nothing to set in your environment \u2014 the data does not exist on this machine. Connect to the hosted MCP at ${HOSTED_MCP_URL}, or open the console at https://app.carrier.llc, to use this tool. Every OCS tool in this server works against your own token and is unaffected.`
|
|
7112
|
+
);
|
|
7113
|
+
}
|
|
7114
|
+
|
|
7115
|
+
// src/stripe-connect-tools.ts
|
|
7184
7116
|
async function verifyConfirmToken(env, sub, toolName, token2) {
|
|
7117
|
+
requireBinding(
|
|
7118
|
+
env.OAUTH_KV,
|
|
7119
|
+
toolName,
|
|
7120
|
+
"OAUTH_KV",
|
|
7121
|
+
"the short-lived confirmation token this operation requires"
|
|
7122
|
+
);
|
|
7185
7123
|
const key = `confirm:${sub}:${toolName}`;
|
|
7186
7124
|
const stored = await env.OAUTH_KV.get(key);
|
|
7187
7125
|
if (!stored || stored !== token2) return false;
|
|
@@ -7189,6 +7127,12 @@ async function verifyConfirmToken(env, sub, toolName, token2) {
|
|
|
7189
7127
|
return true;
|
|
7190
7128
|
}
|
|
7191
7129
|
async function issueConfirmToken(env, sub, toolName) {
|
|
7130
|
+
requireBinding(
|
|
7131
|
+
env.OAUTH_KV,
|
|
7132
|
+
toolName,
|
|
7133
|
+
"OAUTH_KV",
|
|
7134
|
+
"the short-lived confirmation token this operation requires"
|
|
7135
|
+
);
|
|
7192
7136
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
7193
7137
|
const token2 = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7194
7138
|
const key = `confirm:${sub}:${toolName}`;
|
|
@@ -8316,6 +8260,7 @@ var PRICING_TOOLS = [
|
|
|
8316
8260
|
required: []
|
|
8317
8261
|
},
|
|
8318
8262
|
handler: async (env, props2) => {
|
|
8263
|
+
requireUserStore(env, "credit_balance", "your MCP plan credit ledger");
|
|
8319
8264
|
const tier = props2.tier;
|
|
8320
8265
|
const summary = await getCreditSummary(env, props2.sub, tier);
|
|
8321
8266
|
const creditCheck = await checkCredits(env, props2.sub, tier, "read");
|
|
@@ -8397,6 +8342,7 @@ var PRICING_TOOLS = [
|
|
|
8397
8342
|
required: []
|
|
8398
8343
|
},
|
|
8399
8344
|
handler: async (env, props2, args) => {
|
|
8345
|
+
requireUserStore(env, "configure_billing", "your billing configuration");
|
|
8400
8346
|
const tier = props2.tier;
|
|
8401
8347
|
if (tier === "free") {
|
|
8402
8348
|
return {
|
|
@@ -8465,6 +8411,11 @@ var PRICING_TOOLS = [
|
|
|
8465
8411
|
required: []
|
|
8466
8412
|
},
|
|
8467
8413
|
handler: async (env, props2, args) => {
|
|
8414
|
+
requireUserStore(
|
|
8415
|
+
env,
|
|
8416
|
+
"usage_projection",
|
|
8417
|
+
"the consumption history a projection is drawn from"
|
|
8418
|
+
);
|
|
8468
8419
|
const tier = props2.tier;
|
|
8469
8420
|
const summary = await getCreditSummary(env, props2.sub, tier);
|
|
8470
8421
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -8613,6 +8564,7 @@ var PRICING_TOOLS = [
|
|
|
8613
8564
|
required: []
|
|
8614
8565
|
},
|
|
8615
8566
|
handler: async (env, props2, args) => {
|
|
8567
|
+
requireUserStore(env, "billing_events", "your billing event history");
|
|
8616
8568
|
const limit = Math.min(
|
|
8617
8569
|
typeof args.limit === "number" ? args.limit : 20,
|
|
8618
8570
|
50
|
|
@@ -8923,6 +8875,11 @@ var PROJECTS_TOOLS = [
|
|
|
8923
8875
|
required: ["new_token", "confirm"]
|
|
8924
8876
|
},
|
|
8925
8877
|
handler: async (env, props2, args) => {
|
|
8878
|
+
requireUserStore(
|
|
8879
|
+
env,
|
|
8880
|
+
"rotate_credentials",
|
|
8881
|
+
"the encrypted token vault this writes to"
|
|
8882
|
+
);
|
|
8926
8883
|
if (!args.confirm) {
|
|
8927
8884
|
return {
|
|
8928
8885
|
status: "cancelled",
|
|
@@ -8937,7 +8894,9 @@ var PROJECTS_TOOLS = [
|
|
|
8937
8894
|
}
|
|
8938
8895
|
const encryptionKey = env.CARRIER_TOKEN_ENCRYPTION_KEY;
|
|
8939
8896
|
if (!encryptionKey) {
|
|
8940
|
-
|
|
8897
|
+
throw new Error(
|
|
8898
|
+
"rotate_credentials cannot run here: CARRIER_TOKEN_ENCRYPTION_KEY is a secret of the hosted Carrier service and is not present in a local install, so there is no way to encrypt a token for the vault. Rotate from the console at https://app.carrier.llc, or connect to the hosted MCP at https://mcp.carrier.llc/mcp. In local stdio mode the OCS token comes from CARRIER_OCS_API_TOKEN in your own environment \u2014 change it there and restart, no rotation call needed."
|
|
8899
|
+
);
|
|
8941
8900
|
}
|
|
8942
8901
|
const encrypted = await encryptToken(newToken, encryptionKey);
|
|
8943
8902
|
const orgId = props2.org_id;
|
|
@@ -9086,13 +9045,17 @@ var PROJECTS_TOOLS = [
|
|
|
9086
9045
|
handler: async (env, props2) => {
|
|
9087
9046
|
const sub = props2.sub;
|
|
9088
9047
|
const orgId = props2.org_id;
|
|
9048
|
+
const hasStore = !!env.CARRIER_USERS;
|
|
9089
9049
|
let orgDetails = null;
|
|
9090
|
-
if (orgId) {
|
|
9050
|
+
if (orgId && hasStore) {
|
|
9091
9051
|
orgDetails = await env.CARRIER_USERS.get(`org:${orgId}`, "json").catch(() => null);
|
|
9092
9052
|
}
|
|
9093
|
-
const hasStripe = !!await env.CARRIER_USERS.get(`stripe_customer_id:${sub}`);
|
|
9094
|
-
const hasWebhook = !!await env.CARRIER_USERS.get(`webhook_url:${sub}`);
|
|
9053
|
+
const hasStripe = hasStore ? !!await env.CARRIER_USERS.get(`stripe_customer_id:${sub}`) : null;
|
|
9054
|
+
const hasWebhook = hasStore ? !!await env.CARRIER_USERS.get(`webhook_url:${sub}`) : null;
|
|
9095
9055
|
return {
|
|
9056
|
+
...hasStore ? {} : {
|
|
9057
|
+
note: "Running as a local stdio server. Fields reported as null are held by the hosted Carrier service and cannot be read from here; connect to https://mcp.carrier.llc/mcp to see them. Everything else below is this session's own configuration and is accurate."
|
|
9058
|
+
},
|
|
9096
9059
|
environment: {
|
|
9097
9060
|
user_sub: sub,
|
|
9098
9061
|
org_id: orgId ?? null,
|
|
@@ -9517,8 +9480,8 @@ var CURATED_ROUTER_TOOLS = [
|
|
|
9517
9480
|
},
|
|
9518
9481
|
{
|
|
9519
9482
|
name: "move_subscriber_range_to_account",
|
|
9520
|
-
description: "Move a range of subscribers
|
|
9521
|
-
input_schema: { type: "object", properties: {
|
|
9483
|
+
description: "Move a range of subscribers (by IMSI or ICCID) from one account to another. Intent: 'move subscribers', 'transfer SIMs to account'.",
|
|
9484
|
+
input_schema: { type: "object", properties: { srcAccountId: { type: "number" }, destAccount: { type: "number" }, rangeType: { type: "string" } } }
|
|
9522
9485
|
},
|
|
9523
9486
|
{
|
|
9524
9487
|
name: "hlr_get_bitrate",
|
|
@@ -9801,15 +9764,28 @@ var ROUTER_SYSTEM_PROMPT = `You are a carrier fleet operations router. Your job
|
|
|
9801
9764
|
|
|
9802
9765
|
${ROUTER_RULES}`;
|
|
9803
9766
|
var ROUTER_TIMEOUT_MS = 12e3;
|
|
9767
|
+
var AskRouterNotConfiguredError = class extends Error {
|
|
9768
|
+
constructor(missing) {
|
|
9769
|
+
super(routerRemediation(missing));
|
|
9770
|
+
this.missing = missing;
|
|
9771
|
+
this.name = "AskRouterNotConfiguredError";
|
|
9772
|
+
}
|
|
9773
|
+
missing;
|
|
9774
|
+
};
|
|
9775
|
+
function missingRouterCredentials(env) {
|
|
9776
|
+
const missing = [];
|
|
9777
|
+
if (!env.AWS_ACCESS_KEY_ID) missing.push("AWS_ACCESS_KEY_ID");
|
|
9778
|
+
if (!env.AWS_SECRET_ACCESS_KEY) missing.push("AWS_SECRET_ACCESS_KEY");
|
|
9779
|
+
if (env.CARRIER_ASK_ENABLED !== "true") missing.push("CARRIER_ASK_ENABLED=true");
|
|
9780
|
+
return missing;
|
|
9781
|
+
}
|
|
9782
|
+
function routerRemediation(missing) {
|
|
9783
|
+
return `carrier_ask cannot route: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not set. Set them in the environment of the process running this MCP server (for Claude Desktop / Cursor / Windsurf, the \`env\` block of the server entry in your MCP config), then restart it. The router calls Amazon Bedrock, so the credentials must belong to an IAM identity with bedrock:InvokeModel. Every other Carrier tool works without this \u2014 carrier_ask only picks which one to call, so you can call the tool you want directly in the meantime.`;
|
|
9784
|
+
}
|
|
9804
9785
|
async function _routeIntent(intent, context, env, opts = {}) {
|
|
9805
9786
|
const { attribution, onUsage } = opts;
|
|
9806
9787
|
if (env.CARRIER_ASK_ENABLED !== "true" || !env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY) {
|
|
9807
|
-
|
|
9808
|
-
match: "routing_pending",
|
|
9809
|
-
intent_received: intent,
|
|
9810
|
-
note: "carrier_ask routing engine is not yet activated. Add AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY to Doppler carrier/dev+stg+prd and set CARRIER_ASK_ENABLED=true.",
|
|
9811
|
-
scaffold_version: "v2.0-bedrock"
|
|
9812
|
-
};
|
|
9788
|
+
throw new AskRouterNotConfiguredError(missingRouterCredentials(env));
|
|
9813
9789
|
}
|
|
9814
9790
|
const definedContext = Object.fromEntries(
|
|
9815
9791
|
Object.entries(context).filter(([, value]) => value !== void 0)
|
|
@@ -10065,6 +10041,34 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
10065
10041
|
}
|
|
10066
10042
|
});
|
|
10067
10043
|
} catch (err7) {
|
|
10044
|
+
if (err7 instanceof AskRouterNotConfiguredError) {
|
|
10045
|
+
writeCarrierAskAudit(ctx.env, {
|
|
10046
|
+
intent_hash: intentHash,
|
|
10047
|
+
match: "error",
|
|
10048
|
+
resolved_tool: "none",
|
|
10049
|
+
confirm_token_state: "none",
|
|
10050
|
+
status: "error",
|
|
10051
|
+
latency_ms: Date.now() - start,
|
|
10052
|
+
sub: ctx.props.sub,
|
|
10053
|
+
reseller_id: ctx.props.reseller_id,
|
|
10054
|
+
model_id: "none",
|
|
10055
|
+
tier: ctx.props.tier,
|
|
10056
|
+
usage: ZERO_USAGE
|
|
10057
|
+
});
|
|
10058
|
+
return {
|
|
10059
|
+
content: [
|
|
10060
|
+
{
|
|
10061
|
+
type: "text",
|
|
10062
|
+
text: JSON.stringify({
|
|
10063
|
+
error: "router_not_configured",
|
|
10064
|
+
missing: err7.missing,
|
|
10065
|
+
message: err7.message
|
|
10066
|
+
})
|
|
10067
|
+
}
|
|
10068
|
+
],
|
|
10069
|
+
isError: true
|
|
10070
|
+
};
|
|
10071
|
+
}
|
|
10068
10072
|
writeCarrierAskAudit(ctx.env, {
|
|
10069
10073
|
intent_hash: intentHash,
|
|
10070
10074
|
match: "error",
|
|
@@ -10422,6 +10426,12 @@ function registerListRecentOcsEventsTool(server2, ctx) {
|
|
|
10422
10426
|
};
|
|
10423
10427
|
}
|
|
10424
10428
|
}
|
|
10429
|
+
requireBinding(
|
|
10430
|
+
env.OCS_EVENT_ROUTING,
|
|
10431
|
+
"list_recent_ocs_events",
|
|
10432
|
+
"OCS_EVENT_ROUTING",
|
|
10433
|
+
"the OCS event ring buffer"
|
|
10434
|
+
);
|
|
10425
10435
|
const result2 = await listRecentOcsEvents2(
|
|
10426
10436
|
iccid,
|
|
10427
10437
|
limit,
|
|
@@ -10578,7 +10588,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10578
10588
|
const kv = ctx.env.RATE_LIMIT_KV;
|
|
10579
10589
|
if (!kv) {
|
|
10580
10590
|
return err5(
|
|
10581
|
-
"
|
|
10591
|
+
"subscriber_depletion_events cannot answer here. Depletion events are written by the Carrier webhook receiver as they arrive from the network and kept for 7 days in hosted storage; a local stdio server receives no webhooks and has nowhere to keep them, so there is no history on this machine to read. There is nothing to set in your environment. Use the hosted MCP at https://mcp.carrier.llc/mcp for this tool. To see a subscriber's current bundle state instead of its depletion history, `get_subscriber` and `list_subscriber_packages` query OCS directly and work here."
|
|
10582
10592
|
);
|
|
10583
10593
|
}
|
|
10584
10594
|
const { subscriberId, since } = args;
|
|
@@ -12233,43 +12243,228 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12233
12243
|
|
|
12234
12244
|
// src/tools-ui-agent-generic.ts
|
|
12235
12245
|
import { z as z22 } from "zod";
|
|
12236
|
-
var
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12246
|
+
var COST_PER_STEP_USD = 0.014;
|
|
12247
|
+
var SESSION_OVERHEAD_USD = 5e-3;
|
|
12248
|
+
function estimateCostUsd2(steps) {
|
|
12249
|
+
return Math.round((COST_PER_STEP_USD * steps + SESSION_OVERHEAD_USD) * 100) / 100;
|
|
12250
|
+
}
|
|
12251
|
+
function notConfiguredError(toolName) {
|
|
12252
|
+
return {
|
|
12253
|
+
isError: true,
|
|
12254
|
+
content: [
|
|
12255
|
+
{
|
|
12256
|
+
type: "text",
|
|
12257
|
+
text: JSON.stringify({
|
|
12258
|
+
error: "browser_use_not_configured",
|
|
12259
|
+
tool: toolName,
|
|
12260
|
+
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."
|
|
12261
|
+
})
|
|
12262
|
+
}
|
|
12263
|
+
]
|
|
12264
|
+
};
|
|
12265
|
+
}
|
|
12266
|
+
function scopeError2(toolName, required, actual) {
|
|
12267
|
+
return {
|
|
12268
|
+
isError: true,
|
|
12269
|
+
content: [
|
|
12270
|
+
{
|
|
12271
|
+
type: "text",
|
|
12272
|
+
text: JSON.stringify({
|
|
12273
|
+
error: "scope_denied",
|
|
12274
|
+
message: `Scope denied: tool '${toolName}' requires '${required}' scope. Your token has: [${actual.join(", ")}].`
|
|
12275
|
+
})
|
|
12276
|
+
}
|
|
12277
|
+
]
|
|
12278
|
+
};
|
|
12279
|
+
}
|
|
12280
|
+
function registerUiAgentGenericTools(server2, ctx, runtime) {
|
|
12250
12281
|
server2.registerTool(
|
|
12251
12282
|
"ui_agent_ask",
|
|
12252
12283
|
{
|
|
12253
|
-
title: "Dispatch Generic
|
|
12254
|
-
description: "
|
|
12284
|
+
title: "Dispatch Generic Browsing Agent",
|
|
12285
|
+
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.",
|
|
12255
12286
|
inputSchema: {
|
|
12256
|
-
prompt: z22.string().min(1).max(4e3).describe(
|
|
12257
|
-
|
|
12258
|
-
|
|
12287
|
+
prompt: z22.string().min(1).max(4e3).describe(
|
|
12288
|
+
"Natural-language task description for the browsing agent. Be specific: include target URLs, data to extract, or actions to perform."
|
|
12289
|
+
),
|
|
12290
|
+
max_steps: z22.number().int().min(1).max(30).optional().describe(
|
|
12291
|
+
"Advisory step budget (1\u201330, default 15). Prices the dry-run estimate; the hosted agent sets its own depth."
|
|
12292
|
+
),
|
|
12293
|
+
dry_run: z22.boolean().optional().describe(
|
|
12294
|
+
"Preview cost estimate without dispatching. Returns estimated_cost_usd only \u2014 no browser session is created."
|
|
12295
|
+
)
|
|
12259
12296
|
}
|
|
12260
12297
|
},
|
|
12261
|
-
async (
|
|
12298
|
+
async (args) => {
|
|
12299
|
+
const start = Date.now();
|
|
12300
|
+
const toolName = "ui_agent_ask";
|
|
12301
|
+
const { prompt, max_steps = 15, dry_run = false } = args;
|
|
12302
|
+
if (!ctx.props.scope.includes("write") && !ctx.props.scope.includes("admin")) {
|
|
12303
|
+
ctx.audit({
|
|
12304
|
+
tool_name: toolName,
|
|
12305
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12306
|
+
status: "scope_denied",
|
|
12307
|
+
dry_run: false,
|
|
12308
|
+
duration_ms: 0
|
|
12309
|
+
});
|
|
12310
|
+
return scopeError2(toolName, "write", ctx.props.scope);
|
|
12311
|
+
}
|
|
12312
|
+
if (dry_run) {
|
|
12313
|
+
return {
|
|
12314
|
+
content: [
|
|
12315
|
+
{
|
|
12316
|
+
type: "text",
|
|
12317
|
+
text: JSON.stringify(
|
|
12318
|
+
{
|
|
12319
|
+
status: "dry_run",
|
|
12320
|
+
estimated_cost_usd: estimateCostUsd2(max_steps),
|
|
12321
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
12322
|
+
note: "No browser session was created and nothing was billed."
|
|
12323
|
+
},
|
|
12324
|
+
null,
|
|
12325
|
+
2
|
|
12326
|
+
)
|
|
12327
|
+
}
|
|
12328
|
+
]
|
|
12329
|
+
};
|
|
12330
|
+
}
|
|
12331
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
12332
|
+
return notConfiguredError(toolName);
|
|
12333
|
+
}
|
|
12334
|
+
try {
|
|
12335
|
+
runtime.budget.reserve();
|
|
12336
|
+
} catch (err7) {
|
|
12337
|
+
if (err7 instanceof SpendLimitError) {
|
|
12338
|
+
ctx.audit({
|
|
12339
|
+
tool_name: toolName,
|
|
12340
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12341
|
+
// The existing quota status, not a new one: AuditRow's status is a
|
|
12342
|
+
// closed union shared with the Worker, and a stdio-only value would
|
|
12343
|
+
// be a row shape the hosted surface never emits.
|
|
12344
|
+
status: "quota_exceeded",
|
|
12345
|
+
dry_run: false,
|
|
12346
|
+
duration_ms: 0,
|
|
12347
|
+
event_type: "ui_agent_dispatch"
|
|
12348
|
+
});
|
|
12349
|
+
return {
|
|
12350
|
+
isError: true,
|
|
12351
|
+
content: [
|
|
12352
|
+
{
|
|
12353
|
+
type: "text",
|
|
12354
|
+
text: JSON.stringify({
|
|
12355
|
+
error: "run_limit_reached",
|
|
12356
|
+
message: err7.message
|
|
12357
|
+
})
|
|
12358
|
+
}
|
|
12359
|
+
]
|
|
12360
|
+
};
|
|
12361
|
+
}
|
|
12362
|
+
throw err7;
|
|
12363
|
+
}
|
|
12364
|
+
try {
|
|
12365
|
+
const result2 = await runUiAgent(
|
|
12366
|
+
prompt,
|
|
12367
|
+
"Carrier CLI browsing agent",
|
|
12368
|
+
runtime.env
|
|
12369
|
+
);
|
|
12370
|
+
runtime.dispatchedTaskIds.add(result2.task_id);
|
|
12371
|
+
ctx.audit({
|
|
12372
|
+
tool_name: toolName,
|
|
12373
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12374
|
+
status: result2.status === "error" ? "error" : "ok",
|
|
12375
|
+
dry_run: false,
|
|
12376
|
+
duration_ms: Date.now() - start,
|
|
12377
|
+
event_type: "ui_agent_dispatch"
|
|
12378
|
+
});
|
|
12379
|
+
return {
|
|
12380
|
+
...result2.status === "error" ? { isError: true } : {},
|
|
12381
|
+
content: [
|
|
12382
|
+
{
|
|
12383
|
+
type: "text",
|
|
12384
|
+
text: JSON.stringify(
|
|
12385
|
+
{
|
|
12386
|
+
status: result2.status,
|
|
12387
|
+
task_id: result2.task_id,
|
|
12388
|
+
...result2.session_viewer_url ? { session_viewer_url: result2.session_viewer_url } : {},
|
|
12389
|
+
result: result2.result,
|
|
12390
|
+
session_spend_usd: runtime.spend.costUsd,
|
|
12391
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
12392
|
+
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."
|
|
12393
|
+
},
|
|
12394
|
+
null,
|
|
12395
|
+
2
|
|
12396
|
+
)
|
|
12397
|
+
}
|
|
12398
|
+
]
|
|
12399
|
+
};
|
|
12400
|
+
} catch (err7) {
|
|
12401
|
+
ctx.audit({
|
|
12402
|
+
tool_name: toolName,
|
|
12403
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12404
|
+
status: "error",
|
|
12405
|
+
dry_run: false,
|
|
12406
|
+
duration_ms: Date.now() - start,
|
|
12407
|
+
event_type: "ui_agent_dispatch"
|
|
12408
|
+
});
|
|
12409
|
+
return {
|
|
12410
|
+
isError: true,
|
|
12411
|
+
content: [
|
|
12412
|
+
{
|
|
12413
|
+
type: "text",
|
|
12414
|
+
text: JSON.stringify({
|
|
12415
|
+
error: "ui_agent_failed",
|
|
12416
|
+
message: err7 instanceof Error ? err7.message : String(err7)
|
|
12417
|
+
})
|
|
12418
|
+
}
|
|
12419
|
+
]
|
|
12420
|
+
};
|
|
12421
|
+
}
|
|
12422
|
+
}
|
|
12262
12423
|
);
|
|
12263
12424
|
server2.registerTool(
|
|
12264
12425
|
"ui_agent_status",
|
|
12265
12426
|
{
|
|
12266
|
-
title: "
|
|
12267
|
-
description: "
|
|
12427
|
+
title: "Poll Browsing Agent Task Status",
|
|
12428
|
+
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.",
|
|
12268
12429
|
inputSchema: {
|
|
12269
|
-
task_id: z22.string().describe("
|
|
12430
|
+
task_id: z22.string().describe("Task ID returned by ui_agent_ask")
|
|
12270
12431
|
}
|
|
12271
12432
|
},
|
|
12272
|
-
async (
|
|
12433
|
+
async (args) => {
|
|
12434
|
+
const toolName = "ui_agent_status";
|
|
12435
|
+
if (ctx.props.scope.length === 0) {
|
|
12436
|
+
return scopeError2(toolName, "read", ctx.props.scope);
|
|
12437
|
+
}
|
|
12438
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
12439
|
+
return notConfiguredError(toolName);
|
|
12440
|
+
}
|
|
12441
|
+
const task = await getUiAgentTask(
|
|
12442
|
+
args.task_id,
|
|
12443
|
+
runtime.env
|
|
12444
|
+
);
|
|
12445
|
+
if (!task) {
|
|
12446
|
+
return {
|
|
12447
|
+
isError: true,
|
|
12448
|
+
content: [
|
|
12449
|
+
{
|
|
12450
|
+
type: "text",
|
|
12451
|
+
text: JSON.stringify({
|
|
12452
|
+
error: "task_not_found",
|
|
12453
|
+
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.`
|
|
12454
|
+
})
|
|
12455
|
+
}
|
|
12456
|
+
]
|
|
12457
|
+
};
|
|
12458
|
+
}
|
|
12459
|
+
return {
|
|
12460
|
+
content: [
|
|
12461
|
+
{
|
|
12462
|
+
type: "text",
|
|
12463
|
+
text: JSON.stringify(task, null, 2)
|
|
12464
|
+
}
|
|
12465
|
+
]
|
|
12466
|
+
};
|
|
12467
|
+
}
|
|
12273
12468
|
);
|
|
12274
12469
|
}
|
|
12275
12470
|
|
|
@@ -12295,8 +12490,15 @@ var stdioEnv = {
|
|
|
12295
12490
|
ASSETS: null,
|
|
12296
12491
|
CARRIER_USERS: null,
|
|
12297
12492
|
DOWNLOADS: null,
|
|
12298
|
-
MANUS_API_KEY: "",
|
|
12299
12493
|
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY ?? "",
|
|
12494
|
+
// Browser Use Cloud. Read from the environment with a real fallback, the way
|
|
12495
|
+
// STRIPE_SECRET_KEY above and the wallet values below are — unlike the
|
|
12496
|
+
// MANUS_API_KEY this replaces, which was hardcoded to "" with no fallback at
|
|
12497
|
+
// all, so every ui_agent_* tool returned a configuration error to every
|
|
12498
|
+
// caller from the day it shipped. Absent is still a valid state: the tools
|
|
12499
|
+
// stay registered and say so. Runs are billed to this key at ~$0.20-1.00
|
|
12500
|
+
// each, so dispatch is capped per process — see ui-agent-runtime.ts.
|
|
12501
|
+
BROWSER_USE_API_KEY: process.env.BROWSER_USE_API_KEY,
|
|
12300
12502
|
// Prepaid-wallet configuration. Read from the environment so a local operator
|
|
12301
12503
|
// who holds these values gets working wallet tools; when absent the tools stay
|
|
12302
12504
|
// registered (catalog parity with the Worker) and return an explicit
|
|
@@ -12304,7 +12506,23 @@ var stdioEnv = {
|
|
|
12304
12506
|
ATLAS_BASE_URL: process.env.ATLAS_BASE_URL,
|
|
12305
12507
|
CARRIER_INTERNAL_API_KEY: process.env.CARRIER_INTERNAL_API_KEY,
|
|
12306
12508
|
WALLET_CHECKOUT_SUCCESS_URL: process.env.WALLET_CHECKOUT_SUCCESS_URL,
|
|
12307
|
-
WALLET_CHECKOUT_CANCEL_URL: process.env.WALLET_CHECKOUT_CANCEL_URL
|
|
12509
|
+
WALLET_CHECKOUT_CANCEL_URL: process.env.WALLET_CHECKOUT_CANCEL_URL,
|
|
12510
|
+
// Bedrock credentials for carrier_ask, the natural-language router. None of
|
|
12511
|
+
// these appeared in this shim at all until now, so every read saw `undefined`
|
|
12512
|
+
// and the routing gate in tools-carrier-ask.ts could never open: the flagship
|
|
12513
|
+
// router had routed nothing from npm since it shipped. Same treatment as
|
|
12514
|
+
// STRIPE_SECRET_KEY and the wallet values above — read the environment, and
|
|
12515
|
+
// when absent let the tool refuse out loud rather than pretend it answered.
|
|
12516
|
+
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
|
|
12517
|
+
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
|
|
12518
|
+
AWS_REGION: process.env.AWS_REGION,
|
|
12519
|
+
BEDROCK_MODEL_ID: process.env.BEDROCK_MODEL_ID,
|
|
12520
|
+
// Defaults ON here, unlike the Worker. The flag is the Worker's staged
|
|
12521
|
+
// rollout switch; a local operator who has put AWS credentials on the process
|
|
12522
|
+
// has already opted in, and making them set a second variable they have never
|
|
12523
|
+
// heard of is just the same dead gate one step further along. Setting it to
|
|
12524
|
+
// anything other than "true" still turns the router off.
|
|
12525
|
+
CARRIER_ASK_ENABLED: process.env.CARRIER_ASK_ENABLED ?? "true"
|
|
12308
12526
|
};
|
|
12309
12527
|
var props = {
|
|
12310
12528
|
sub: "stdio@local",
|
|
@@ -12340,13 +12558,16 @@ registerCountryHistoryTool(server, toolCtx);
|
|
|
12340
12558
|
registerDepletionEventsTool(server, toolCtx);
|
|
12341
12559
|
registerAllApps(server, toolCtx);
|
|
12342
12560
|
registerAllPricingTools(server, toolCtx);
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12561
|
+
var uiAgentRuntime = createUiAgentRuntime();
|
|
12562
|
+
var portalOptIn = readPortalOptIn();
|
|
12563
|
+
warnPortalOptIn(portalOptIn);
|
|
12564
|
+
registerScheduleAndUsageTools(server, toolCtx, uiAgentRuntime);
|
|
12565
|
+
registerAllUiAgentTools(server, toolCtx, uiAgentRuntime, portalOptIn);
|
|
12566
|
+
registerUiAgentAskTools(server, toolCtx, uiAgentRuntime);
|
|
12346
12567
|
registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
12347
12568
|
registerWalletTools(server, { env: stdioEnv, props });
|
|
12348
12569
|
registerGreenzoneTools(server, toolCtx);
|
|
12349
|
-
registerUiAgentGenericTools(server, toolCtx);
|
|
12570
|
+
registerUiAgentGenericTools(server, toolCtx, uiAgentRuntime);
|
|
12350
12571
|
registerStorefrontLogoTools(
|
|
12351
12572
|
server,
|
|
12352
12573
|
stdioEnv,
|