@carrierllc/mcp 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/{chunk-KHNJNJX3.js → chunk-DUAENMJE.js} +2 -2
- package/dist/chunk-DUAENMJE.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1256 -1204
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-KHNJNJX3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
usageOverPeriodParams,
|
|
48
48
|
verifyStorefront,
|
|
49
49
|
withOcsListSummary
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-DUAENMJE.js";
|
|
51
51
|
import "./chunk-SHKKVIIA.js";
|
|
52
52
|
|
|
53
53
|
// src/index.ts
|
|
@@ -360,6 +360,469 @@ async function invokeModelRaw(creds, payload, opts = {}, signal) {
|
|
|
360
360
|
signal?.removeEventListener("abort", onAbort);
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
+
var PENDING_ASK_PREFIX = "manus_pending_ask:";
|
|
364
|
+
var PENDING_ASK_TTL_SECONDS = 24 * 3600;
|
|
365
|
+
var OCS_DASHBOARD_DEFAULT = "https://ocs.esimvault.cloud";
|
|
366
|
+
function buildLoginPreamble(dashboardUrl) {
|
|
367
|
+
return `STEP 1 \u2014 LOGIN:
|
|
368
|
+
Navigate to ${dashboardUrl}/login (or the main page if no /login path).
|
|
369
|
+
Enter the OCS portal username and password provided below.
|
|
370
|
+
Wait for the dashboard to fully load after login.
|
|
371
|
+
If already logged in (session cookie persists), skip to STEP 2.
|
|
372
|
+
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
function assembleUiAgentPrompt(operationPrompt, credentials, dashboardUrl) {
|
|
376
|
+
return `You are a Carrier MCP UI automation agent. Your task is to perform an OCS dashboard operation that is not available via the OCS REST API.
|
|
377
|
+
|
|
378
|
+
OCS PORTAL CREDENTIALS (use these to log in \u2014 NEVER include them in your output):
|
|
379
|
+
Username: ${credentials.username}
|
|
380
|
+
Password: ${credentials.password}
|
|
381
|
+
|
|
382
|
+
` + buildLoginPreamble(dashboardUrl) + `STEP 2 \u2014 OPERATION:
|
|
383
|
+
` + operationPrompt + `
|
|
384
|
+
|
|
385
|
+
STEP 3 \u2014 VERIFICATION:
|
|
386
|
+
After completing the operation, verify the result by checking the dashboard shows the expected state.
|
|
387
|
+
Take a screenshot of the final state for audit purposes.
|
|
388
|
+
Report success or failure with a clear summary.`;
|
|
389
|
+
}
|
|
390
|
+
function redactCredentials(prompt, credentials) {
|
|
391
|
+
return prompt.replaceAll(credentials.password, "***REDACTED***").replaceAll(credentials.username, "***REDACTED***");
|
|
392
|
+
}
|
|
393
|
+
function buildCreateSteeringListPrompt(params, dashboardUrl) {
|
|
394
|
+
return `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
395
|
+
Create a new steering list with the following details:
|
|
396
|
+
Name: ${params.name}
|
|
397
|
+
` + (params.description ? ` Description: ${params.description}
|
|
398
|
+
` : "") + `Click the "Create" or "Add" button to create the steering list.
|
|
399
|
+
After creation, note the new steering list ID from the dashboard.
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
function buildBuildSteeringListPrompt(params, dashboardUrl) {
|
|
403
|
+
const add = params.add_operators ?? [];
|
|
404
|
+
const remove = params.remove_operators ?? [];
|
|
405
|
+
return `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
406
|
+
Open steering list ID ${params.steering_list_id} for editing.
|
|
407
|
+
Operator type: ${params.operator_type ?? "priority"}
|
|
408
|
+
` + (add.length > 0 ? `Add the following operators: ${add.join(", ")}
|
|
409
|
+
` : "") + (remove.length > 0 ? `Remove the following operators: ${remove.join(", ")}
|
|
410
|
+
` : "") + `Save the changes and verify the updated operator list.
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
function buildSetAccountSteeringListPrompt(params, dashboardUrl) {
|
|
414
|
+
return `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
|
|
415
|
+
Open account ID ${params.account_id}.
|
|
416
|
+
` + (params.steering_list_id === 0 ? `Remove/unset the steering list assignment from this account.
|
|
417
|
+
` : `Assign steering list ID ${params.steering_list_id} to this account.
|
|
418
|
+
`) + `Save the changes and verify the steering list assignment is updated.
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
function buildCreateAccountPrompt(params, dashboardUrl) {
|
|
422
|
+
return `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
|
|
423
|
+
Create a new account with the following details:
|
|
424
|
+
Name: ${params.name}
|
|
425
|
+
` + (params.description ? ` Description: ${params.description}
|
|
426
|
+
` : "") + (params.initial_balance ? ` Initial balance: ${params.initial_balance}
|
|
427
|
+
` : "") + `Click the "Create" or "Add" button.
|
|
428
|
+
After creation, note the new account ID from the dashboard.
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
function buildCreateDestinationListPrompt(params, dashboardUrl) {
|
|
432
|
+
const prefixes = params.prefixes ?? [];
|
|
433
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
434
|
+
Create a new destination list with the following details:
|
|
435
|
+
Name: ${params.name}
|
|
436
|
+
` + (params.description ? ` Description: ${params.description}
|
|
437
|
+
` : "") + (prefixes.length > 0 ? ` Prefixes to add: ${prefixes.join(", ")}
|
|
438
|
+
` : "") + `Save the new destination list and note the ID.
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
function buildEditDestinationListPrompt(params, dashboardUrl) {
|
|
442
|
+
const add = params.add_prefixes ?? [];
|
|
443
|
+
const remove = params.remove_prefixes ?? [];
|
|
444
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
445
|
+
Open destination list ID ${params.destination_list_id} for editing.
|
|
446
|
+
` + (params.new_name ? `Rename to: ${params.new_name}
|
|
447
|
+
` : "") + (add.length > 0 ? `Add prefixes: ${add.join(", ")}
|
|
448
|
+
` : "") + (remove.length > 0 ? `Remove prefixes: ${remove.join(", ")}
|
|
449
|
+
` : "") + `Save the changes and verify the updated prefix list.
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
function buildDeleteDestinationListPrompt(params, dashboardUrl) {
|
|
453
|
+
return `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
|
|
454
|
+
Find destination list ID ${params.destination_list_id}.
|
|
455
|
+
Delete this destination list. Confirm the deletion when prompted.
|
|
456
|
+
Verify the list no longer appears in the dashboard.
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
function buildDeletePackageTemplatePrompt(params, dashboardUrl) {
|
|
460
|
+
return `Navigate to the Package Templates section of the OCS dashboard at ${dashboardUrl}.
|
|
461
|
+
Find package template ID ${params.template_id}.
|
|
462
|
+
Delete this package template. Confirm the deletion when prompted.
|
|
463
|
+
Verify the template no longer appears in the template list.
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
function buildEditLocationZonePrompt(params, dashboardUrl) {
|
|
467
|
+
const add = params.add_countries ?? [];
|
|
468
|
+
const remove = params.remove_countries ?? [];
|
|
469
|
+
return `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
|
|
470
|
+
Open location zone ID ${params.zone_id} for editing.
|
|
471
|
+
` + (params.new_name ? `Rename to: ${params.new_name}
|
|
472
|
+
` : "") + (add.length > 0 ? `Add countries: ${add.join(", ")}
|
|
473
|
+
` : "") + (remove.length > 0 ? `Remove countries: ${remove.join(", ")}
|
|
474
|
+
` : "") + `Save the changes and verify the updated country list.
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
function buildDeleteLocationZonePrompt(params, dashboardUrl) {
|
|
478
|
+
return `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
|
|
479
|
+
Find location zone ID ${params.zone_id}.
|
|
480
|
+
Delete this location zone. Confirm the deletion when prompted.
|
|
481
|
+
If the dashboard shows an error (e.g. zone in use by active templates), report the error.
|
|
482
|
+
Verify the zone no longer appears in the zone list.
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
var BROWSER_USE_API = "https://api.browser-use.com/api/v4";
|
|
486
|
+
var RUN_TIMEOUT_MS = 10 * 6e4;
|
|
487
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
488
|
+
function browserUseConfigured(env) {
|
|
489
|
+
return Boolean(env.apiKey);
|
|
490
|
+
}
|
|
491
|
+
function apiKey(env) {
|
|
492
|
+
const key = env.apiKey;
|
|
493
|
+
if (!key) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
"BROWSER_USE_API_KEY is not set \u2014 the browser-use provider cannot run."
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return key;
|
|
499
|
+
}
|
|
500
|
+
async function v4(env, path, init) {
|
|
501
|
+
const res = await fetch(`${BROWSER_USE_API}${path}`, {
|
|
502
|
+
method: init?.method ?? "GET",
|
|
503
|
+
headers: {
|
|
504
|
+
"X-Browser-Use-API-Key": apiKey(env),
|
|
505
|
+
"Content-Type": "application/json"
|
|
506
|
+
},
|
|
507
|
+
...init?.body ? { body: JSON.stringify(init.body) } : {}
|
|
508
|
+
});
|
|
509
|
+
let data = null;
|
|
510
|
+
try {
|
|
511
|
+
data = await res.json();
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
return { ok: res.ok, status: res.status, data };
|
|
515
|
+
}
|
|
516
|
+
function mapStatus(raw) {
|
|
517
|
+
switch ((raw ?? "").toLowerCase()) {
|
|
518
|
+
case "finished":
|
|
519
|
+
case "completed":
|
|
520
|
+
case "success":
|
|
521
|
+
case "stopped":
|
|
522
|
+
return "stopped";
|
|
523
|
+
case "failed":
|
|
524
|
+
case "error":
|
|
525
|
+
return "error";
|
|
526
|
+
case "paused":
|
|
527
|
+
case "waiting":
|
|
528
|
+
case "needs_input":
|
|
529
|
+
return "waiting";
|
|
530
|
+
default:
|
|
531
|
+
return "running";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function defaultRedact(text) {
|
|
535
|
+
return text.replace(/(\bpassword\b\s*[:=]\s*)\S+/gi, "$1***REDACTED***").replace(/(\busername\b\s*[:=]\s*)\S+/gi, "$1***REDACTED***");
|
|
536
|
+
}
|
|
537
|
+
function toTaskResult(run, redact) {
|
|
538
|
+
const status = mapStatus(run.status);
|
|
539
|
+
const scrub = (text) => defaultRedact(redact ? redact(text) : text);
|
|
540
|
+
const payload = run.result ?? run.output;
|
|
541
|
+
if (payload && typeof payload === "object") {
|
|
542
|
+
const p = payload;
|
|
543
|
+
return {
|
|
544
|
+
success: status === "stopped" && !run.error,
|
|
545
|
+
summary: scrub(
|
|
546
|
+
typeof p.summary === "string" ? p.summary : JSON.stringify(payload)
|
|
547
|
+
).slice(0, 2e3),
|
|
548
|
+
...typeof p.entity_id === "string" ? { entity_id: p.entity_id } : {},
|
|
549
|
+
...run.error ? { error_message: scrub(run.error) } : {},
|
|
550
|
+
// The hosted agent does not report a screenshot count. Zero is honest;
|
|
551
|
+
// inventing one would put a fabricated number in an audit row.
|
|
552
|
+
screenshots_taken: 0
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
success: status === "stopped" && !run.error,
|
|
557
|
+
summary: typeof payload === "string" ? scrub(payload).slice(0, 2e3) : "",
|
|
558
|
+
...run.error ? { error_message: scrub(run.error) } : {},
|
|
559
|
+
screenshots_taken: 0
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function viewerUrl(run) {
|
|
563
|
+
return run.sessionId ? `https://cloud.browser-use.com/sessions/${run.sessionId}` : void 0;
|
|
564
|
+
}
|
|
565
|
+
function runSpend(run) {
|
|
566
|
+
if (!run || typeof run.totalCostUsd !== "number") return null;
|
|
567
|
+
return {
|
|
568
|
+
costUsd: run.totalCostUsd,
|
|
569
|
+
inputTokens: run.totalInputTokens ?? 0,
|
|
570
|
+
outputTokens: run.totalOutputTokens ?? 0
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function createRun(env, task, opts) {
|
|
574
|
+
const body2 = { task };
|
|
575
|
+
if (opts?.model) body2.model = opts.model;
|
|
576
|
+
if (opts?.outputSchema) body2.schema = opts.outputSchema;
|
|
577
|
+
const res = await v4(env, "/runs", { method: "POST", body: body2 });
|
|
578
|
+
if (!res.ok) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Browser Use run create failed: HTTP ${res.status} ${JSON.stringify(res.data).slice(0, 300)}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const run = res.data;
|
|
584
|
+
if (!run?.id) throw new Error("Browser Use run create returned no id");
|
|
585
|
+
const url = viewerUrl(run);
|
|
586
|
+
return { runId: run.id, ...url ? { sessionViewerUrl: url } : {} };
|
|
587
|
+
}
|
|
588
|
+
async function getRun(env, runId) {
|
|
589
|
+
const res = await v4(env, `/runs/${encodeURIComponent(runId)}`);
|
|
590
|
+
if (!res.ok) return null;
|
|
591
|
+
return res.data;
|
|
592
|
+
}
|
|
593
|
+
async function waitForRun(env, runId, timeoutMs = RUN_TIMEOUT_MS) {
|
|
594
|
+
const deadline = Date.now() + timeoutMs;
|
|
595
|
+
let last = null;
|
|
596
|
+
while (Date.now() < deadline) {
|
|
597
|
+
last = await getRun(env, runId);
|
|
598
|
+
if (last && mapStatus(last.status) !== "running") return last;
|
|
599
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
600
|
+
}
|
|
601
|
+
return last;
|
|
602
|
+
}
|
|
603
|
+
function toSteelTask(taskId, run, opts) {
|
|
604
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
605
|
+
const status = mapStatus(run?.status);
|
|
606
|
+
const url = run ? viewerUrl(run) : void 0;
|
|
607
|
+
return {
|
|
608
|
+
task_id: taskId,
|
|
609
|
+
status,
|
|
610
|
+
...opts?.ownerSub ? { owner_sub: opts.ownerSub } : {},
|
|
611
|
+
// The console's task detail view renders steel_session_id. It is the
|
|
612
|
+
// provider's session id, whoever the provider is.
|
|
613
|
+
...run?.sessionId ? { steel_session_id: run.sessionId } : {},
|
|
614
|
+
...run?.id ? { run_id: run.id } : {},
|
|
615
|
+
...url ? { session_viewer_url: url } : {},
|
|
616
|
+
...opts?.title ? { title: opts.title } : {},
|
|
617
|
+
created_at: opts?.createdAt ?? now,
|
|
618
|
+
updated_at: now,
|
|
619
|
+
...run && status !== "running" ? { result: toTaskResult(run, opts?.redact) } : {},
|
|
620
|
+
// Scrubbed on the same grounds as the result: the question is agent-authored
|
|
621
|
+
// text, and an agent that pauses to ask for help is exactly the one likely
|
|
622
|
+
// to quote its own instructions — password line included — while explaining
|
|
623
|
+
// what it is stuck on. This string is stored in KV and handed to operators
|
|
624
|
+
// by ui_agent_list_pending.
|
|
625
|
+
...status === "waiting" ? { pending_question: pendingQuestion(run, opts?.redact) } : {}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function pendingQuestion(run, redact) {
|
|
629
|
+
const payload = run?.result;
|
|
630
|
+
const q = payload && typeof payload === "object" ? payload.question : void 0;
|
|
631
|
+
if (typeof q !== "string" || q.length === 0) {
|
|
632
|
+
return "The browser agent is paused and needs input to continue.";
|
|
633
|
+
}
|
|
634
|
+
return defaultRedact(redact ? redact(q) : q);
|
|
635
|
+
}
|
|
636
|
+
function taskKey(taskId) {
|
|
637
|
+
return `steel_task:${taskId}`;
|
|
638
|
+
}
|
|
639
|
+
var TASK_TTL_SECONDS = 7 * 24 * 3600;
|
|
640
|
+
async function beginUiAgentRun(prompt, title, env, opts) {
|
|
641
|
+
if (!browserUseConfigured(env)) {
|
|
642
|
+
throw new Error("BROWSER_USE_API_KEY is not configured");
|
|
643
|
+
}
|
|
644
|
+
const taskId = opts?.taskId ?? `bu_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
645
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
646
|
+
const base = {
|
|
647
|
+
task_id: taskId,
|
|
648
|
+
status: "running",
|
|
649
|
+
title,
|
|
650
|
+
created_at: createdAt,
|
|
651
|
+
updated_at: createdAt,
|
|
652
|
+
...opts?.ownerSub ? { owner_sub: opts.ownerSub } : {}
|
|
653
|
+
};
|
|
654
|
+
await putTask(env, base);
|
|
655
|
+
let created;
|
|
656
|
+
try {
|
|
657
|
+
created = await createRun(env, prompt, {
|
|
658
|
+
...opts?.model ? { model: opts.model } : {},
|
|
659
|
+
...opts?.outputSchema ? { outputSchema: opts.outputSchema } : {}
|
|
660
|
+
});
|
|
661
|
+
} catch (err7) {
|
|
662
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
663
|
+
await putTask(env, {
|
|
664
|
+
...base,
|
|
665
|
+
status: "error",
|
|
666
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
667
|
+
result: {
|
|
668
|
+
success: false,
|
|
669
|
+
summary: `Failed to start a Browser Use run: ${message}`,
|
|
670
|
+
error_message: message,
|
|
671
|
+
screenshots_taken: 0
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
throw err7;
|
|
675
|
+
}
|
|
676
|
+
const dispatched = {
|
|
677
|
+
...base,
|
|
678
|
+
run_id: created.runId,
|
|
679
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
680
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
681
|
+
};
|
|
682
|
+
await putTask(env, dispatched);
|
|
683
|
+
const complete = async () => {
|
|
684
|
+
let run;
|
|
685
|
+
try {
|
|
686
|
+
run = await waitForRun(env, created.runId);
|
|
687
|
+
} catch (err7) {
|
|
688
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
689
|
+
const failed = {
|
|
690
|
+
success: false,
|
|
691
|
+
summary: `Browser Use run failed: ${message}`,
|
|
692
|
+
error_message: message,
|
|
693
|
+
screenshots_taken: 0
|
|
694
|
+
};
|
|
695
|
+
await putTask(env, {
|
|
696
|
+
...dispatched,
|
|
697
|
+
status: "error",
|
|
698
|
+
result: failed,
|
|
699
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
700
|
+
});
|
|
701
|
+
writeAgentAudit(env, {
|
|
702
|
+
event_type: "task_stopped",
|
|
703
|
+
stop_reason: "error",
|
|
704
|
+
task_id: taskId
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
task_id: taskId,
|
|
708
|
+
status: "error",
|
|
709
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
710
|
+
result: failed
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const final = toSteelTask(taskId, run, {
|
|
714
|
+
title,
|
|
715
|
+
...opts?.ownerSub ? { ownerSub: opts.ownerSub } : {},
|
|
716
|
+
...opts?.redact ? { redact: opts.redact } : {},
|
|
717
|
+
createdAt
|
|
718
|
+
});
|
|
719
|
+
if (!final.session_viewer_url && created.sessionViewerUrl) {
|
|
720
|
+
final.session_viewer_url = created.sessionViewerUrl;
|
|
721
|
+
}
|
|
722
|
+
final.run_id = created.runId;
|
|
723
|
+
await putTask(env, final);
|
|
724
|
+
if (final.status === "waiting") {
|
|
725
|
+
await writePendingAsk(env, final);
|
|
726
|
+
}
|
|
727
|
+
const spend = runSpend(run);
|
|
728
|
+
writeAgentAudit(env, {
|
|
729
|
+
event_type: "task_stopped",
|
|
730
|
+
stop_reason: final.status === "waiting" ? "ask" : final.status === "stopped" ? "finish" : final.status === "running" ? "timeout" : "error",
|
|
731
|
+
task_id: taskId,
|
|
732
|
+
...spend ? { spend } : {}
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
task_id: taskId,
|
|
736
|
+
status: final.status,
|
|
737
|
+
...final.session_viewer_url ? { session_viewer_url: final.session_viewer_url } : {},
|
|
738
|
+
...final.result ? { result: final.result } : {},
|
|
739
|
+
...spend ? { spend } : {}
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
task_id: taskId,
|
|
744
|
+
...created.sessionViewerUrl ? { session_viewer_url: created.sessionViewerUrl } : {},
|
|
745
|
+
complete
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async function runUiAgent(prompt, title, env, opts) {
|
|
749
|
+
const handle = await beginUiAgentRun(prompt, title, env, opts);
|
|
750
|
+
return await handle.complete();
|
|
751
|
+
}
|
|
752
|
+
async function getUiAgentTask(taskId, env) {
|
|
753
|
+
const raw = await env.store.get(taskKey(taskId));
|
|
754
|
+
if (!raw) return null;
|
|
755
|
+
try {
|
|
756
|
+
return JSON.parse(raw);
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function resumeUiAgentTask(taskId, reply, env) {
|
|
762
|
+
const pendingRaw = await env.store.get(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
763
|
+
if (!pendingRaw) {
|
|
764
|
+
return { ok: false, error: "No pending-ask entry found for this task_id." };
|
|
765
|
+
}
|
|
766
|
+
const task = await getUiAgentTask(taskId, env);
|
|
767
|
+
if (!task) {
|
|
768
|
+
return { ok: false, error: "Task record not found." };
|
|
769
|
+
}
|
|
770
|
+
await env.store.delete(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
771
|
+
const { pending_question: _dropped, ...rest } = task;
|
|
772
|
+
await putTask(env, {
|
|
773
|
+
...rest,
|
|
774
|
+
status: "running",
|
|
775
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
776
|
+
});
|
|
777
|
+
writeAgentAudit(env, {
|
|
778
|
+
event_type: "task_stopped",
|
|
779
|
+
stop_reason: "resumed",
|
|
780
|
+
task_id: taskId,
|
|
781
|
+
replyLen: reply.length
|
|
782
|
+
});
|
|
783
|
+
return { ok: true };
|
|
784
|
+
}
|
|
785
|
+
async function putTask(env, task) {
|
|
786
|
+
await env.store.put(taskKey(task.task_id), JSON.stringify(task), {
|
|
787
|
+
expirationTtl: TASK_TTL_SECONDS
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
async function writePendingAsk(env, task) {
|
|
791
|
+
const entry = {
|
|
792
|
+
task_id: task.task_id,
|
|
793
|
+
question: task.pending_question ?? "The browser agent is paused and needs input to continue.",
|
|
794
|
+
task_url: task.session_viewer_url ?? "",
|
|
795
|
+
asked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
796
|
+
};
|
|
797
|
+
await env.store.put(
|
|
798
|
+
`${PENDING_ASK_PREFIX}${task.task_id}`,
|
|
799
|
+
JSON.stringify(entry),
|
|
800
|
+
{ expirationTtl: PENDING_ASK_TTL_SECONDS }
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
function writeAgentAudit(env, entry) {
|
|
804
|
+
try {
|
|
805
|
+
env.audit({
|
|
806
|
+
blobs: [
|
|
807
|
+
"steel_agent",
|
|
808
|
+
entry.event_type,
|
|
809
|
+
entry.stop_reason,
|
|
810
|
+
entry.task_id,
|
|
811
|
+
"0",
|
|
812
|
+
"browser_use"
|
|
813
|
+
],
|
|
814
|
+
doubles: [
|
|
815
|
+
entry.replyLen ?? 0,
|
|
816
|
+
entry.spend?.costUsd ?? 0,
|
|
817
|
+
entry.spend?.inputTokens ?? 0,
|
|
818
|
+
entry.spend?.outputTokens ?? 0
|
|
819
|
+
],
|
|
820
|
+
indexes: [entry.task_id]
|
|
821
|
+
});
|
|
822
|
+
} catch (err7) {
|
|
823
|
+
console.error(`[browser-use-agent] audit write failed: ${err7.message}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
363
826
|
|
|
364
827
|
// src/billing.ts
|
|
365
828
|
var TIER_CALL_LIMITS = {
|
|
@@ -4888,246 +5351,358 @@ import { z as z11 } from "zod";
|
|
|
4888
5351
|
// src/tools-ui-agent.ts
|
|
4889
5352
|
import { z as z4 } from "zod";
|
|
4890
5353
|
|
|
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() {
|
|
5354
|
+
// src/ui-agent-stub.ts
|
|
5355
|
+
var REMOTE_MCP_URL = "https://mcp.carrier.llc/mcp";
|
|
5356
|
+
var REASON_TEXT = {
|
|
5357
|
+
ocs_portal_credentials: "drives the OCS web dashboard, which needs OCS portal login credentials. Those are stored in Clerk private metadata and read through the Clerk Backend API, which the local CLI has no secret for.",
|
|
5358
|
+
recurring_runtime: "needs a recurring runtime. Browser Use Cloud has no native scheduler, so the hosted Worker implements schedules with KV plus a Cloudflare Cron Trigger and leader election. A CLI process exits when the client disconnects, so a schedule created here would never fire."
|
|
5359
|
+
};
|
|
5360
|
+
function uiAgentStubError(toolName, reason) {
|
|
4953
5361
|
return {
|
|
4954
5362
|
isError: true,
|
|
4955
5363
|
content: [
|
|
4956
5364
|
{
|
|
4957
5365
|
type: "text",
|
|
4958
5366
|
text: JSON.stringify({
|
|
4959
|
-
error: "
|
|
4960
|
-
|
|
4961
|
-
|
|
5367
|
+
error: "requires_worker_runtime",
|
|
5368
|
+
tool: toolName,
|
|
5369
|
+
reason,
|
|
5370
|
+
message: `${toolName} ${REASON_TEXT[reason]} Connect to the remote Carrier MCP Worker to use this tool. Generic web browsing (ui_agent_ask / ui_agent_status) does work locally \u2014 set BROWSER_USE_API_KEY.`,
|
|
5371
|
+
remote_url: REMOTE_MCP_URL
|
|
4962
5372
|
})
|
|
4963
5373
|
}
|
|
4964
5374
|
]
|
|
4965
5375
|
};
|
|
4966
5376
|
}
|
|
4967
|
-
|
|
5377
|
+
|
|
5378
|
+
// src/portal-credentials.ts
|
|
5379
|
+
var PORTAL_OPT_IN_ENV = "CARRIER_ALLOW_PORTAL_PASSWORD";
|
|
5380
|
+
var PORTAL_USERNAME_ENV = "CARRIER_OCS_PORTAL_USERNAME";
|
|
5381
|
+
var PORTAL_PASSWORD_ENV = "CARRIER_OCS_PORTAL_PASSWORD";
|
|
5382
|
+
var PORTAL_DASHBOARD_ENV = "CARRIER_OCS_DASHBOARD_URL";
|
|
5383
|
+
var PORTAL_PASSWORD_WARNING = [
|
|
5384
|
+
`${PORTAL_OPT_IN_ENV} is set: the ten OCS-dashboard tools will use the portal login in`,
|
|
5385
|
+
`${PORTAL_USERNAME_ENV} / ${PORTAL_PASSWORD_ENV}.`,
|
|
5386
|
+
"",
|
|
5387
|
+
"That password is full dashboard access, not a scoped API token. Held in an environment",
|
|
5388
|
+
"variable it is readable from shell history, from `ps` output by any process on this",
|
|
5389
|
+
"machine, and from CI logs if this runs in CI. Carrier never logs it, returns it in tool",
|
|
5390
|
+
"output, or writes it to an audit row, but it is sent to Browser Use Cloud, which types it",
|
|
5391
|
+
"into the OCS login form.",
|
|
5392
|
+
"",
|
|
5393
|
+
"Each dashboard run also costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY.",
|
|
5394
|
+
"",
|
|
5395
|
+
`Unset ${PORTAL_OPT_IN_ENV} to turn this off; the tools then refuse as they did before.`
|
|
5396
|
+
].join("\n");
|
|
5397
|
+
var PORTAL_PASSWORD_RUN_NOTE = `This run used the OCS portal password from ${PORTAL_PASSWORD_ENV}, enabled by ${PORTAL_OPT_IN_ENV}. That credential is full dashboard access and was sent to Browser Use Cloud to type into the OCS login form. It is not logged, audited or returned anywhere in this response.`;
|
|
5398
|
+
function flagSet(raw) {
|
|
5399
|
+
if (raw === void 0) return false;
|
|
5400
|
+
const v = raw.trim().toLowerCase();
|
|
5401
|
+
return v === "1" || v === "true";
|
|
5402
|
+
}
|
|
5403
|
+
function nonEmpty(raw) {
|
|
5404
|
+
if (raw === void 0) return void 0;
|
|
5405
|
+
return raw.length > 0 ? raw : void 0;
|
|
5406
|
+
}
|
|
5407
|
+
function readPortalOptIn(env = process.env) {
|
|
5408
|
+
const flag = flagSet(env[PORTAL_OPT_IN_ENV]);
|
|
5409
|
+
const username = nonEmpty(env[PORTAL_USERNAME_ENV]);
|
|
5410
|
+
const password = nonEmpty(env[PORTAL_PASSWORD_ENV]);
|
|
5411
|
+
const hasCredentials = username !== void 0 && password !== void 0;
|
|
5412
|
+
if (flag && hasCredentials) {
|
|
5413
|
+
return {
|
|
5414
|
+
enabled: true,
|
|
5415
|
+
credentials: { username, password },
|
|
5416
|
+
...nonEmpty(env[PORTAL_DASHBOARD_ENV]) ? { dashboardUrl: env[PORTAL_DASHBOARD_ENV] } : {}
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
if (flag) return { enabled: false, gap: "flag_without_credentials" };
|
|
5420
|
+
if (hasCredentials)
|
|
5421
|
+
return { enabled: false, gap: "credentials_without_flag" };
|
|
5422
|
+
return { enabled: false, gap: "not_requested" };
|
|
5423
|
+
}
|
|
5424
|
+
function warnPortalOptIn(optIn, write = (line) => console.error(line)) {
|
|
5425
|
+
if (optIn.enabled) {
|
|
5426
|
+
write(PORTAL_PASSWORD_WARNING);
|
|
5427
|
+
return;
|
|
5428
|
+
}
|
|
5429
|
+
if (optIn.gap === "flag_without_credentials") {
|
|
5430
|
+
write(PORTAL_PASSWORD_WARNING);
|
|
5431
|
+
write(
|
|
5432
|
+
`${PORTAL_USERNAME_ENV} and ${PORTAL_PASSWORD_ENV} are not both set, so the OCS-dashboard tools stay disabled.`
|
|
5433
|
+
);
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
if (optIn.gap === "credentials_without_flag") {
|
|
5437
|
+
write(
|
|
5438
|
+
`${PORTAL_PASSWORD_ENV} is set but ${PORTAL_OPT_IN_ENV} is not, so the OCS-dashboard tools stay disabled and this portal password is unused. Unset it unless you mean to opt in.`
|
|
5439
|
+
);
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
// src/ui-agent-runtime.ts
|
|
5444
|
+
function createMemoryStore() {
|
|
5445
|
+
const map = /* @__PURE__ */ new Map();
|
|
4968
5446
|
return {
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5447
|
+
async get(key) {
|
|
5448
|
+
const hit = map.get(key);
|
|
5449
|
+
if (!hit) return null;
|
|
5450
|
+
if (hit.expiresAt !== null && Date.now() >= hit.expiresAt) {
|
|
5451
|
+
map.delete(key);
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
return hit.value;
|
|
5455
|
+
},
|
|
5456
|
+
async put(key, value, options) {
|
|
5457
|
+
const ttl = options?.expirationTtl;
|
|
5458
|
+
map.set(key, {
|
|
5459
|
+
value,
|
|
5460
|
+
expiresAt: ttl === void 0 ? null : Date.now() + ttl * 1e3
|
|
5461
|
+
});
|
|
5462
|
+
},
|
|
5463
|
+
async delete(key) {
|
|
5464
|
+
map.delete(key);
|
|
5465
|
+
}
|
|
4979
5466
|
};
|
|
4980
5467
|
}
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5468
|
+
var DEFAULT_MAX_RUNS_PER_PROCESS = 10;
|
|
5469
|
+
var MAX_RUNS_ENV = "CARRIER_BROWSER_USE_MAX_RUNS";
|
|
5470
|
+
var SpendLimitError = class extends Error {
|
|
5471
|
+
constructor(limit) {
|
|
5472
|
+
super(
|
|
5473
|
+
`Browser-agent run limit reached: ${limit} run(s) in this CLI session. Each run costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY. Raise it with ${MAX_RUNS_ENV}=<n>, or 0 to disable the browser agent entirely. Restarting the CLI resets the count.`
|
|
5474
|
+
);
|
|
5475
|
+
this.limit = limit;
|
|
5476
|
+
this.name = "SpendLimitError";
|
|
5477
|
+
}
|
|
5478
|
+
limit;
|
|
5479
|
+
};
|
|
5480
|
+
function maxRunsPerProcess(env = process.env) {
|
|
5481
|
+
const raw = env[MAX_RUNS_ENV];
|
|
5482
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
5483
|
+
return DEFAULT_MAX_RUNS_PER_PROCESS;
|
|
5484
|
+
}
|
|
5485
|
+
const parsed = Number(raw);
|
|
5486
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
5487
|
+
return DEFAULT_MAX_RUNS_PER_PROCESS;
|
|
5488
|
+
}
|
|
5489
|
+
return parsed;
|
|
4989
5490
|
}
|
|
4990
|
-
var
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5491
|
+
var RunBudget = class {
|
|
5492
|
+
constructor(limit) {
|
|
5493
|
+
this.limit = limit;
|
|
5494
|
+
}
|
|
5495
|
+
limit;
|
|
5496
|
+
used = 0;
|
|
5497
|
+
/** Consume one run, or throw. Call immediately before dispatching. */
|
|
5498
|
+
reserve() {
|
|
5499
|
+
if (this.used >= this.limit) throw new SpendLimitError(this.limit);
|
|
5500
|
+
this.used += 1;
|
|
5501
|
+
}
|
|
5502
|
+
get remaining() {
|
|
5503
|
+
return Math.max(0, this.limit - this.used);
|
|
5504
|
+
}
|
|
5505
|
+
get spent() {
|
|
5506
|
+
return this.used;
|
|
5507
|
+
}
|
|
5000
5508
|
};
|
|
5001
|
-
function
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
status: "scope_denied",
|
|
5010
|
-
dry_run: false,
|
|
5011
|
-
duration_ms: 0
|
|
5012
|
-
});
|
|
5013
|
-
return {
|
|
5014
|
-
isError: true,
|
|
5015
|
-
content: [
|
|
5016
|
-
{
|
|
5017
|
-
type: "text",
|
|
5018
|
-
text: `Scope denied: tool '${toolName}' requires '${requiredScope}' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
5019
|
-
}
|
|
5020
|
-
]
|
|
5021
|
-
};
|
|
5509
|
+
function createUiAgentRuntime(processEnv = process.env) {
|
|
5510
|
+
const apiKey2 = processEnv.BROWSER_USE_API_KEY;
|
|
5511
|
+
const spend = { runs: 0, costUsd: 0 };
|
|
5512
|
+
const audit2 = (row) => {
|
|
5513
|
+
const cost = row.doubles?.[1];
|
|
5514
|
+
if (typeof cost === "number" && cost > 0) {
|
|
5515
|
+
spend.runs += 1;
|
|
5516
|
+
spend.costUsd = Math.round((spend.costUsd + cost) * 1e6) / 1e6;
|
|
5022
5517
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
Username: ${creds.username}
|
|
5037
|
-
Password: ${creds.password}
|
|
5038
|
-
|
|
5039
|
-
` + buildLoginPreamble(dashboardUrl) + `STEP 2 \u2014 OPERATION:
|
|
5040
|
-
` + prompt + `
|
|
5518
|
+
};
|
|
5519
|
+
return {
|
|
5520
|
+
env: {
|
|
5521
|
+
...apiKey2 ? { apiKey: apiKey2 } : {},
|
|
5522
|
+
store: createMemoryStore(),
|
|
5523
|
+
audit: audit2
|
|
5524
|
+
},
|
|
5525
|
+
budget: new RunBudget(maxRunsPerProcess(processEnv)),
|
|
5526
|
+
spend,
|
|
5527
|
+
configured: Boolean(apiKey2),
|
|
5528
|
+
dispatchedTaskIds: /* @__PURE__ */ new Set()
|
|
5529
|
+
};
|
|
5530
|
+
}
|
|
5041
5531
|
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5532
|
+
// src/tools-ui-agent.ts
|
|
5533
|
+
var UI_AGENT_TOOL_SCOPES = {
|
|
5534
|
+
ui_create_steering_list: "write",
|
|
5535
|
+
ui_build_steering_list: "write",
|
|
5536
|
+
ui_set_account_steering_list: "write",
|
|
5537
|
+
ui_request_reseller_relay_change: "write",
|
|
5538
|
+
ui_create_account: "admin",
|
|
5539
|
+
ui_create_destination_list: "write",
|
|
5540
|
+
ui_edit_destination_list: "write",
|
|
5541
|
+
ui_delete_destination_list: "admin",
|
|
5542
|
+
ui_delete_package_template: "admin",
|
|
5543
|
+
ui_edit_location_zone: "write",
|
|
5544
|
+
ui_delete_location_zone: "admin"
|
|
5545
|
+
};
|
|
5546
|
+
var STDIO_NOTE_DISABLED = " Requires the remote Carrier MCP Worker deployment \u2014 the local CLI holds no OCS portal credentials.";
|
|
5547
|
+
var STDIO_NOTE_ENABLED = " Runs locally: CARRIER_ALLOW_PORTAL_PASSWORD is set and OCS portal credentials are supplied, so this drives the dashboard from this machine. Each run costs roughly $0.20-1.00 against your own BROWSER_USE_API_KEY and counts against CARRIER_BROWSER_USE_MAX_RUNS.";
|
|
5548
|
+
function stdioNote(optIn) {
|
|
5549
|
+
return optIn.enabled ? STDIO_NOTE_ENABLED : STDIO_NOTE_DISABLED;
|
|
5550
|
+
}
|
|
5551
|
+
function portalStub(toolName) {
|
|
5552
|
+
return uiAgentStubError(toolName, "ocs_portal_credentials");
|
|
5553
|
+
}
|
|
5554
|
+
function jsonResult(body2, isError = false) {
|
|
5555
|
+
return {
|
|
5556
|
+
...isError ? { isError: true } : {},
|
|
5557
|
+
content: [{ type: "text", text: JSON.stringify(body2, null, 2) }]
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5560
|
+
function wrapPortalHandler(toolName, gapId, requiredScope, ctx, runtime, optIn, buildPrompt) {
|
|
5561
|
+
return async (args) => {
|
|
5562
|
+
const start = Date.now();
|
|
5563
|
+
const ocsMethod = `[ui-agent:${gapId}]`;
|
|
5564
|
+
if (!ctx.props.scope.includes(requiredScope)) {
|
|
5048
5565
|
ctx.audit({
|
|
5049
5566
|
tool_name: toolName,
|
|
5050
|
-
ocs_method:
|
|
5051
|
-
status: "
|
|
5052
|
-
dry_run:
|
|
5053
|
-
duration_ms: 0
|
|
5054
|
-
event_type: "ui_agent_dispatch"
|
|
5567
|
+
ocs_method: ocsMethod,
|
|
5568
|
+
status: "scope_denied",
|
|
5569
|
+
dry_run: false,
|
|
5570
|
+
duration_ms: 0
|
|
5055
5571
|
});
|
|
5056
5572
|
return {
|
|
5573
|
+
isError: true,
|
|
5057
5574
|
content: [
|
|
5058
5575
|
{
|
|
5059
5576
|
type: "text",
|
|
5060
|
-
text:
|
|
5061
|
-
dry_run: true,
|
|
5062
|
-
tool: toolName,
|
|
5063
|
-
gap_id: gapId,
|
|
5064
|
-
agent_prompt_preview: redactedPrompt,
|
|
5065
|
-
note: "No Manus agent was dispatched. Set dry_run=false to execute."
|
|
5066
|
-
}, null, 2)
|
|
5577
|
+
text: `Scope denied: tool '${toolName}' requires '${requiredScope}' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
5067
5578
|
}
|
|
5068
5579
|
]
|
|
5069
5580
|
};
|
|
5070
5581
|
}
|
|
5582
|
+
if (!optIn.enabled) return portalStub(toolName);
|
|
5583
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
5584
|
+
return jsonResult(
|
|
5585
|
+
{
|
|
5586
|
+
error: "browser_use_not_configured",
|
|
5587
|
+
tool: toolName,
|
|
5588
|
+
message: "BROWSER_USE_API_KEY is not set, so the dashboard agent cannot run. Set it in the environment of the process running this MCP server. Runs are billed to that key at roughly $0.20-1.00 each."
|
|
5589
|
+
},
|
|
5590
|
+
true
|
|
5591
|
+
);
|
|
5592
|
+
}
|
|
5593
|
+
const { credentials } = optIn;
|
|
5594
|
+
const dashboardUrl = optIn.dashboardUrl ?? OCS_DASHBOARD_DEFAULT;
|
|
5595
|
+
const redact = (text) => redactCredentials(text, credentials);
|
|
5596
|
+
const fullPrompt = assembleUiAgentPrompt(
|
|
5597
|
+
buildPrompt(args, dashboardUrl),
|
|
5598
|
+
credentials,
|
|
5599
|
+
dashboardUrl
|
|
5600
|
+
);
|
|
5601
|
+
if (args.dry_run === true) {
|
|
5602
|
+
ctx.audit({
|
|
5603
|
+
tool_name: toolName,
|
|
5604
|
+
ocs_method: ocsMethod,
|
|
5605
|
+
status: "dry_run",
|
|
5606
|
+
dry_run: true,
|
|
5607
|
+
duration_ms: 0,
|
|
5608
|
+
event_type: "ui_agent_dispatch"
|
|
5609
|
+
});
|
|
5610
|
+
return jsonResult({
|
|
5611
|
+
dry_run: true,
|
|
5612
|
+
tool: toolName,
|
|
5613
|
+
gap_id: gapId,
|
|
5614
|
+
agent_prompt_preview: redact(fullPrompt),
|
|
5615
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
5616
|
+
note: "No browser agent was dispatched. Set dry_run=false to execute.",
|
|
5617
|
+
portal_password_note: PORTAL_PASSWORD_RUN_NOTE
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
try {
|
|
5621
|
+
runtime.budget.reserve();
|
|
5622
|
+
} catch (err7) {
|
|
5623
|
+
if (err7 instanceof SpendLimitError) {
|
|
5624
|
+
ctx.audit({
|
|
5625
|
+
tool_name: toolName,
|
|
5626
|
+
ocs_method: ocsMethod,
|
|
5627
|
+
// The existing quota status, not a new one: AuditRow's status is a
|
|
5628
|
+
// closed union shared with the Worker.
|
|
5629
|
+
status: "quota_exceeded",
|
|
5630
|
+
dry_run: false,
|
|
5631
|
+
duration_ms: 0,
|
|
5632
|
+
event_type: "ui_agent_dispatch"
|
|
5633
|
+
});
|
|
5634
|
+
return jsonResult(
|
|
5635
|
+
{ error: "run_limit_reached", message: err7.message },
|
|
5636
|
+
true
|
|
5637
|
+
);
|
|
5638
|
+
}
|
|
5639
|
+
throw err7;
|
|
5640
|
+
}
|
|
5071
5641
|
try {
|
|
5072
|
-
const result2 = await
|
|
5073
|
-
ctx.env.MANUS_API_KEY,
|
|
5642
|
+
const result2 = await runUiAgent(
|
|
5074
5643
|
fullPrompt,
|
|
5075
|
-
`Carrier
|
|
5076
|
-
|
|
5644
|
+
`Carrier CLI UI Agent: ${toolName} (${gapId})`,
|
|
5645
|
+
runtime.env,
|
|
5646
|
+
{ ownerSub: ctx.props.sub, redact }
|
|
5077
5647
|
);
|
|
5078
|
-
if (!result2.
|
|
5648
|
+
if (!result2.task_id) {
|
|
5079
5649
|
ctx.audit({
|
|
5080
5650
|
tool_name: toolName,
|
|
5081
|
-
ocs_method:
|
|
5651
|
+
ocs_method: ocsMethod,
|
|
5082
5652
|
status: "error",
|
|
5083
5653
|
dry_run: false,
|
|
5084
5654
|
duration_ms: Date.now() - start,
|
|
5085
5655
|
event_type: "ui_agent_dispatch"
|
|
5086
5656
|
});
|
|
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
|
-
};
|
|
5657
|
+
return jsonResult(
|
|
5658
|
+
{
|
|
5659
|
+
error: "steel_dispatch_failed",
|
|
5660
|
+
message: "Failed to create a browser agent task"
|
|
5661
|
+
},
|
|
5662
|
+
true
|
|
5663
|
+
);
|
|
5100
5664
|
}
|
|
5665
|
+
runtime.dispatchedTaskIds.add(result2.task_id);
|
|
5101
5666
|
ctx.audit({
|
|
5102
5667
|
tool_name: toolName,
|
|
5103
|
-
ocs_method:
|
|
5104
|
-
status: "ui_agent_dispatched",
|
|
5668
|
+
ocs_method: ocsMethod,
|
|
5669
|
+
status: result2.status === "error" ? "error" : "ui_agent_dispatched",
|
|
5105
5670
|
dry_run: false,
|
|
5106
5671
|
duration_ms: Date.now() - start,
|
|
5107
5672
|
event_type: "ui_agent_dispatch",
|
|
5108
5673
|
manus_task_id: result2.task_id
|
|
5674
|
+
// AE blob position is the contract; the name is history
|
|
5675
|
+
});
|
|
5676
|
+
return jsonResult({
|
|
5677
|
+
status: result2.status === "stopped" ? "ui_agent_completed" : result2.status === "waiting" ? "ui_agent_awaiting_input" : result2.status,
|
|
5678
|
+
tool: toolName,
|
|
5679
|
+
gap_id: gapId,
|
|
5680
|
+
// Held still across the vendor swap and across transports: MCP clients
|
|
5681
|
+
// and the console read these names.
|
|
5682
|
+
steel_task_id: result2.task_id,
|
|
5683
|
+
steel_session_viewer_url: result2.session_viewer_url,
|
|
5684
|
+
// Redacted defensively. `runUiAgent` already applies the same redactor
|
|
5685
|
+
// before the record is stored, so this is the second of two passes over
|
|
5686
|
+
// the fields an agent can echo its instructions into. Field-by-field
|
|
5687
|
+
// rather than over the serialised JSON: a password containing a quote
|
|
5688
|
+
// or a backslash is escaped by JSON.stringify and would no longer match
|
|
5689
|
+
// a plain substring replace.
|
|
5690
|
+
result: result2.result ? {
|
|
5691
|
+
...result2.result,
|
|
5692
|
+
summary: redact(result2.result.summary),
|
|
5693
|
+
...result2.result.entity_id !== void 0 ? { entity_id: redact(result2.result.entity_id) } : {},
|
|
5694
|
+
...result2.result.error_message !== void 0 ? { error_message: redact(result2.result.error_message) } : {}
|
|
5695
|
+
} : void 0,
|
|
5696
|
+
cost_usd: result2.spend?.costUsd,
|
|
5697
|
+
session_spend_usd: runtime.spend.costUsd,
|
|
5698
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
5699
|
+
note: "The run completed in-process \u2014 a local CLI has no background runtime, so this call blocked for its duration.",
|
|
5700
|
+
portal_password_note: PORTAL_PASSWORD_RUN_NOTE
|
|
5109
5701
|
});
|
|
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
5702
|
} catch (err7) {
|
|
5128
5703
|
ctx.audit({
|
|
5129
5704
|
tool_name: toolName,
|
|
5130
|
-
ocs_method:
|
|
5705
|
+
ocs_method: ocsMethod,
|
|
5131
5706
|
status: "error",
|
|
5132
5707
|
dry_run: false,
|
|
5133
5708
|
duration_ms: Date.now() - start,
|
|
@@ -5138,57 +5713,44 @@ Report success or failure with a clear summary.`;
|
|
|
5138
5713
|
content: [
|
|
5139
5714
|
{
|
|
5140
5715
|
type: "text",
|
|
5141
|
-
text:
|
|
5716
|
+
text: redact(
|
|
5717
|
+
`Error dispatching the UI agent: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
5718
|
+
)
|
|
5142
5719
|
}
|
|
5143
5720
|
]
|
|
5144
5721
|
};
|
|
5145
5722
|
}
|
|
5146
5723
|
};
|
|
5147
5724
|
}
|
|
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) {
|
|
5725
|
+
function registerAllUiAgentTools(server2, ctx, runtime, optIn = readPortalOptIn()) {
|
|
5726
|
+
const NOTE = stdioNote(optIn);
|
|
5727
|
+
const gated = (toolName, gapId, scope, buildPrompt) => wrapPortalHandler(toolName, gapId, scope, ctx, runtime, optIn, buildPrompt);
|
|
5162
5728
|
server2.registerTool(
|
|
5163
5729
|
"ui_create_steering_list",
|
|
5164
5730
|
{
|
|
5165
5731
|
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
|
|
5732
|
+
description: "Creates a new network steering list (OPLMN preference configuration) via the OCS web dashboard. This operation is not available via the OCS REST API. A browser agent is dispatched to perform the operation. Params: `name` (steering list name), `description` (optional)." + NOTE,
|
|
5167
5733
|
inputSchema: {
|
|
5168
5734
|
name: z4.string().describe("Name for the new steering list"),
|
|
5169
5735
|
description: z4.string().optional().describe("Optional description for the steering list"),
|
|
5170
5736
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5171
5737
|
}
|
|
5172
5738
|
},
|
|
5173
|
-
|
|
5739
|
+
gated(
|
|
5174
5740
|
"ui_create_steering_list",
|
|
5175
5741
|
"G-01",
|
|
5176
5742
|
"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
|
-
`
|
|
5743
|
+
(args, dashboardUrl) => buildCreateSteeringListPrompt(
|
|
5744
|
+
args,
|
|
5745
|
+
dashboardUrl
|
|
5746
|
+
)
|
|
5185
5747
|
)
|
|
5186
5748
|
);
|
|
5187
5749
|
server2.registerTool(
|
|
5188
5750
|
"ui_build_steering_list",
|
|
5189
5751
|
{
|
|
5190
5752
|
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').",
|
|
5753
|
+
description: "Adds or removes operators (MCC-MNC) from an existing steering list via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `steering_list_id`, `add_operators` (array of MCC-MNC to add), `remove_operators` (array to remove), `operator_type` ('priority' or 'excluded')." + NOTE,
|
|
5192
5754
|
inputSchema: {
|
|
5193
5755
|
steering_list_id: z4.number().describe("ID of the steering list to modify"),
|
|
5194
5756
|
add_operators: z4.array(z4.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
|
|
@@ -5197,42 +5759,35 @@ After creation, note the new steering list ID from the dashboard.
|
|
|
5197
5759
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5198
5760
|
}
|
|
5199
5761
|
},
|
|
5200
|
-
|
|
5762
|
+
gated(
|
|
5201
5763
|
"ui_build_steering_list",
|
|
5202
5764
|
"G-02",
|
|
5203
5765
|
"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
|
-
`
|
|
5766
|
+
(args, dashboardUrl) => buildBuildSteeringListPrompt(
|
|
5767
|
+
args,
|
|
5768
|
+
dashboardUrl
|
|
5769
|
+
)
|
|
5212
5770
|
)
|
|
5213
5771
|
);
|
|
5214
5772
|
server2.registerTool(
|
|
5215
5773
|
"ui_set_account_steering_list",
|
|
5216
5774
|
{
|
|
5217
5775
|
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).",
|
|
5776
|
+
description: "Assigns or removes a steering list at the account level via the OCS web dashboard. The subscriber-level counterpart `modify_subscriber_steering_list` is available via API; this account-level operation is UI-only. Params: `account_id`, `steering_list_id` (0 to remove)." + NOTE,
|
|
5219
5777
|
inputSchema: {
|
|
5220
5778
|
account_id: z4.number().describe("Account ID to assign the steering list to"),
|
|
5221
5779
|
steering_list_id: z4.number().describe("Steering list ID to assign (0 to remove/unset)"),
|
|
5222
5780
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5223
5781
|
}
|
|
5224
5782
|
},
|
|
5225
|
-
|
|
5783
|
+
gated(
|
|
5226
5784
|
"ui_set_account_steering_list",
|
|
5227
5785
|
"G-03",
|
|
5228
5786
|
"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
|
-
`
|
|
5787
|
+
(args, dashboardUrl) => buildSetAccountSteeringListPrompt(
|
|
5788
|
+
args,
|
|
5789
|
+
dashboardUrl
|
|
5790
|
+
)
|
|
5236
5791
|
)
|
|
5237
5792
|
);
|
|
5238
5793
|
server2.registerTool(
|
|
@@ -5252,7 +5807,12 @@ Open account ID ${args.account_id}.
|
|
|
5252
5807
|
if (!ctx.props.scope.includes("write")) {
|
|
5253
5808
|
return {
|
|
5254
5809
|
isError: true,
|
|
5255
|
-
content: [
|
|
5810
|
+
content: [
|
|
5811
|
+
{
|
|
5812
|
+
type: "text",
|
|
5813
|
+
text: "Scope denied: tool 'ui_request_reseller_relay_change' requires 'write' scope."
|
|
5814
|
+
}
|
|
5815
|
+
]
|
|
5256
5816
|
};
|
|
5257
5817
|
}
|
|
5258
5818
|
const resellerId = args.reseller_id ?? ctx.props.reseller_id;
|
|
@@ -5264,7 +5824,15 @@ Open account ID ${args.account_id}.
|
|
|
5264
5824
|
if (requested.length === 0) {
|
|
5265
5825
|
return {
|
|
5266
5826
|
isError: true,
|
|
5267
|
-
content: [
|
|
5827
|
+
content: [
|
|
5828
|
+
{
|
|
5829
|
+
type: "text",
|
|
5830
|
+
text: JSON.stringify({
|
|
5831
|
+
error: "relay_state_required",
|
|
5832
|
+
message: "Choose at least one relay flag and desired state."
|
|
5833
|
+
})
|
|
5834
|
+
}
|
|
5835
|
+
]
|
|
5268
5836
|
};
|
|
5269
5837
|
}
|
|
5270
5838
|
const draft = [
|
|
@@ -5284,14 +5852,23 @@ Open account ID ${args.account_id}.
|
|
|
5284
5852
|
event_type: "ui_agent_dispatch"
|
|
5285
5853
|
});
|
|
5286
5854
|
return {
|
|
5287
|
-
content: [
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5855
|
+
content: [
|
|
5856
|
+
{
|
|
5857
|
+
type: "text",
|
|
5858
|
+
text: JSON.stringify(
|
|
5859
|
+
{
|
|
5860
|
+
status: "parent_request_drafted",
|
|
5861
|
+
capability: "parent_controlled",
|
|
5862
|
+
reseller_id: resellerId,
|
|
5863
|
+
parent_reseller: resellerId === 1170 ? "Bridge4IP" : "parent reseller/support",
|
|
5864
|
+
draft,
|
|
5865
|
+
sent: false
|
|
5866
|
+
},
|
|
5867
|
+
null,
|
|
5868
|
+
2
|
|
5869
|
+
)
|
|
5870
|
+
}
|
|
5871
|
+
]
|
|
5295
5872
|
};
|
|
5296
5873
|
}
|
|
5297
5874
|
);
|
|
@@ -5299,7 +5876,7 @@ Open account ID ${args.account_id}.
|
|
|
5299
5876
|
"ui_create_account",
|
|
5300
5877
|
{
|
|
5301
5878
|
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).",
|
|
5879
|
+
description: "Creates a new sub-account under the reseller via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `name` (account name), `description` (optional), `initial_balance` (optional, default 0)." + NOTE,
|
|
5303
5880
|
inputSchema: {
|
|
5304
5881
|
name: z4.string().describe("Name for the new account"),
|
|
5305
5882
|
description: z4.string().optional().describe("Optional description"),
|
|
@@ -5307,26 +5884,21 @@ Open account ID ${args.account_id}.
|
|
|
5307
5884
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5308
5885
|
}
|
|
5309
5886
|
},
|
|
5310
|
-
|
|
5887
|
+
gated(
|
|
5311
5888
|
"ui_create_account",
|
|
5312
5889
|
"G-05",
|
|
5313
5890
|
"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
|
-
`
|
|
5891
|
+
(args, dashboardUrl) => buildCreateAccountPrompt(
|
|
5892
|
+
args,
|
|
5893
|
+
dashboardUrl
|
|
5894
|
+
)
|
|
5323
5895
|
)
|
|
5324
5896
|
);
|
|
5325
5897
|
server2.registerTool(
|
|
5326
5898
|
"ui_create_destination_list",
|
|
5327
5899
|
{
|
|
5328
5900
|
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`.",
|
|
5901
|
+
description: "Creates a new destination list (named set of phone number prefixes for MOC call permissions) via the OCS web dashboard. Params: `name`, `prefixes` (array of prefix strings), `description`." + NOTE,
|
|
5330
5902
|
inputSchema: {
|
|
5331
5903
|
name: z4.string().describe("Name for the new destination list"),
|
|
5332
5904
|
prefixes: z4.array(z4.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
|
|
@@ -5334,25 +5906,21 @@ After creation, note the new account ID from the dashboard.
|
|
|
5334
5906
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5335
5907
|
}
|
|
5336
5908
|
},
|
|
5337
|
-
|
|
5909
|
+
gated(
|
|
5338
5910
|
"ui_create_destination_list",
|
|
5339
5911
|
"G-12",
|
|
5340
5912
|
"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
|
-
`
|
|
5913
|
+
(args, dashboardUrl) => buildCreateDestinationListPrompt(
|
|
5914
|
+
args,
|
|
5915
|
+
dashboardUrl
|
|
5916
|
+
)
|
|
5349
5917
|
)
|
|
5350
5918
|
);
|
|
5351
5919
|
server2.registerTool(
|
|
5352
5920
|
"ui_edit_destination_list",
|
|
5353
5921
|
{
|
|
5354
5922
|
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`.",
|
|
5923
|
+
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`." + NOTE,
|
|
5356
5924
|
inputSchema: {
|
|
5357
5925
|
destination_list_id: z4.number().describe("ID of the destination list to edit"),
|
|
5358
5926
|
add_prefixes: z4.array(z4.string()).optional().describe("Prefixes to add"),
|
|
@@ -5361,69 +5929,61 @@ Create a new destination list with the following details:
|
|
|
5361
5929
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5362
5930
|
}
|
|
5363
5931
|
},
|
|
5364
|
-
|
|
5932
|
+
gated(
|
|
5365
5933
|
"ui_edit_destination_list",
|
|
5366
5934
|
"G-12",
|
|
5367
5935
|
"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
|
-
`
|
|
5936
|
+
(args, dashboardUrl) => buildEditDestinationListPrompt(
|
|
5937
|
+
args,
|
|
5938
|
+
dashboardUrl
|
|
5939
|
+
)
|
|
5376
5940
|
)
|
|
5377
5941
|
);
|
|
5378
5942
|
server2.registerTool(
|
|
5379
5943
|
"ui_delete_destination_list",
|
|
5380
5944
|
{
|
|
5381
5945
|
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.",
|
|
5946
|
+
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone." + NOTE,
|
|
5383
5947
|
inputSchema: {
|
|
5384
5948
|
destination_list_id: z4.number().describe("ID of the destination list to delete"),
|
|
5385
5949
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5386
5950
|
}
|
|
5387
5951
|
},
|
|
5388
|
-
|
|
5952
|
+
gated(
|
|
5389
5953
|
"ui_delete_destination_list",
|
|
5390
5954
|
"G-12",
|
|
5391
5955
|
"admin",
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
Verify the list no longer appears in the dashboard.
|
|
5397
|
-
`
|
|
5956
|
+
(args, dashboardUrl) => buildDeleteDestinationListPrompt(
|
|
5957
|
+
args,
|
|
5958
|
+
dashboardUrl
|
|
5959
|
+
)
|
|
5398
5960
|
)
|
|
5399
5961
|
);
|
|
5400
5962
|
server2.registerTool(
|
|
5401
5963
|
"ui_delete_package_template",
|
|
5402
5964
|
{
|
|
5403
5965
|
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.",
|
|
5966
|
+
description: "Deletes a package template from the product catalog via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `template_id`. WARNING: This is destructive." + NOTE,
|
|
5405
5967
|
inputSchema: {
|
|
5406
5968
|
template_id: z4.number().describe("ID of the package template to delete"),
|
|
5407
5969
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5408
5970
|
}
|
|
5409
5971
|
},
|
|
5410
|
-
|
|
5972
|
+
gated(
|
|
5411
5973
|
"ui_delete_package_template",
|
|
5412
5974
|
"G-18",
|
|
5413
5975
|
"admin",
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
Verify the template no longer appears in the template list.
|
|
5419
|
-
`
|
|
5976
|
+
(args, dashboardUrl) => buildDeletePackageTemplatePrompt(
|
|
5977
|
+
args,
|
|
5978
|
+
dashboardUrl
|
|
5979
|
+
)
|
|
5420
5980
|
)
|
|
5421
5981
|
);
|
|
5422
5982
|
server2.registerTool(
|
|
5423
5983
|
"ui_edit_location_zone",
|
|
5424
5984
|
{
|
|
5425
5985
|
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`.",
|
|
5986
|
+
description: "Edits an existing location zone via the OCS web dashboard. `create_location_zone` is available via API; edit is UI-only. Params: `zone_id`, `new_name`, `add_countries`, `remove_countries`." + NOTE,
|
|
5427
5987
|
inputSchema: {
|
|
5428
5988
|
zone_id: z4.number().describe("ID of the location zone to edit"),
|
|
5429
5989
|
new_name: z4.string().optional().describe("Rename the location zone"),
|
|
@@ -5432,137 +5992,40 @@ Verify the template no longer appears in the template list.
|
|
|
5432
5992
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5433
5993
|
}
|
|
5434
5994
|
},
|
|
5435
|
-
|
|
5995
|
+
gated(
|
|
5436
5996
|
"ui_edit_location_zone",
|
|
5437
5997
|
"G-19",
|
|
5438
5998
|
"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
|
-
`
|
|
5999
|
+
(args, dashboardUrl) => buildEditLocationZonePrompt(
|
|
6000
|
+
args,
|
|
6001
|
+
dashboardUrl
|
|
6002
|
+
)
|
|
5447
6003
|
)
|
|
5448
6004
|
);
|
|
5449
6005
|
server2.registerTool(
|
|
5450
6006
|
"ui_delete_location_zone",
|
|
5451
6007
|
{
|
|
5452
6008
|
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.",
|
|
6009
|
+
description: "Deletes a location zone via the OCS web dashboard. `create_location_zone` is available via API; delete is UI-only. Params: `zone_id`. WARNING: Zones in use by active templates may not be deletable." + NOTE,
|
|
5454
6010
|
inputSchema: {
|
|
5455
6011
|
zone_id: z4.number().describe("ID of the location zone to delete"),
|
|
5456
6012
|
dry_run: z4.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
5457
6013
|
}
|
|
5458
6014
|
},
|
|
5459
|
-
|
|
6015
|
+
gated(
|
|
5460
6016
|
"ui_delete_location_zone",
|
|
5461
6017
|
"G-19",
|
|
5462
6018
|
"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
|
-
`
|
|
6019
|
+
(args, dashboardUrl) => buildDeleteLocationZonePrompt(
|
|
6020
|
+
args,
|
|
6021
|
+
dashboardUrl
|
|
6022
|
+
)
|
|
5470
6023
|
)
|
|
5471
6024
|
);
|
|
5472
6025
|
}
|
|
5473
6026
|
|
|
5474
6027
|
// src/tools-ui-agent-ask.ts
|
|
5475
6028
|
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
6029
|
var UI_AGENT_ASK_TOOL_SCOPES = {
|
|
5567
6030
|
ui_agent_reply: "write",
|
|
5568
6031
|
ui_agent_list_pending: "read"
|
|
@@ -5572,12 +6035,23 @@ function computeExpiresAt(askedAt) {
|
|
|
5572
6035
|
if (isNaN(asked)) return "";
|
|
5573
6036
|
return new Date(asked + PENDING_ASK_TTL_SECONDS * 1e3).toISOString();
|
|
5574
6037
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
6038
|
+
var emptyStore = {
|
|
6039
|
+
get: async () => null,
|
|
6040
|
+
put: async () => void 0,
|
|
6041
|
+
delete: async () => void 0
|
|
6042
|
+
};
|
|
6043
|
+
async function callerOwnsTask(ctx, taskId, fallback) {
|
|
6044
|
+
const key = `steel_task:${taskId}`;
|
|
6045
|
+
let raw = null;
|
|
5577
6046
|
try {
|
|
5578
|
-
raw = await ctx.env.CARRIER_USERS.get(
|
|
6047
|
+
raw = await ctx.env.CARRIER_USERS.get(key);
|
|
5579
6048
|
} catch {
|
|
5580
|
-
return false;
|
|
6049
|
+
if (!fallback) return false;
|
|
6050
|
+
try {
|
|
6051
|
+
raw = await fallback.get(key);
|
|
6052
|
+
} catch {
|
|
6053
|
+
return false;
|
|
6054
|
+
}
|
|
5581
6055
|
}
|
|
5582
6056
|
if (!raw) return false;
|
|
5583
6057
|
try {
|
|
@@ -5587,15 +6061,26 @@ async function callerOwnsTask(ctx, taskId) {
|
|
|
5587
6061
|
return false;
|
|
5588
6062
|
}
|
|
5589
6063
|
}
|
|
5590
|
-
function
|
|
6064
|
+
function listableBinding(ctx) {
|
|
6065
|
+
const kv = ctx.env.CARRIER_USERS;
|
|
6066
|
+
return kv && typeof kv.list === "function" ? kv : null;
|
|
6067
|
+
}
|
|
6068
|
+
function registerUiAgentAskTools(server2, ctx, runtime) {
|
|
6069
|
+
const sharedEnv = () => ({
|
|
6070
|
+
...runtime?.env.apiKey ? { apiKey: runtime.env.apiKey } : {},
|
|
6071
|
+
store: ctx.env.CARRIER_USERS ?? runtime?.env.store ?? emptyStore,
|
|
6072
|
+
audit: runtime?.env.audit ?? (() => void 0)
|
|
6073
|
+
});
|
|
5591
6074
|
server2.registerTool(
|
|
5592
6075
|
"ui_agent_reply",
|
|
5593
6076
|
{
|
|
5594
6077
|
title: "Reply to Paused UI Agent Task",
|
|
5595
|
-
description: "Resumes a
|
|
6078
|
+
description: "Resumes a browser automation task that paused to ask a question. Use ui_agent_list_pending to find tasks waiting for input. Provide the task_id and your reply (e.g. a 2FA code, a field value, or a yes/no answer). The reply content is never recorded in audit logs \u2014 only its length is logged.",
|
|
5596
6079
|
inputSchema: {
|
|
5597
|
-
task_id: z5.string().describe("
|
|
5598
|
-
reply: z5.string().describe(
|
|
6080
|
+
task_id: z5.string().describe("Task ID to resume (from ui_agent_list_pending)"),
|
|
6081
|
+
reply: z5.string().describe(
|
|
6082
|
+
"Your answer to the agent's question (e.g. a 2FA code or confirmation)"
|
|
6083
|
+
)
|
|
5599
6084
|
}
|
|
5600
6085
|
},
|
|
5601
6086
|
async (args) => {
|
|
@@ -5619,24 +6104,14 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5619
6104
|
]
|
|
5620
6105
|
};
|
|
5621
6106
|
}
|
|
5622
|
-
const
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
type: "text",
|
|
5629
|
-
text: JSON.stringify({
|
|
5630
|
-
error: "manus_api_not_configured",
|
|
5631
|
-
message: "MANUS_API_KEY is not configured on this deployment."
|
|
5632
|
-
})
|
|
5633
|
-
}
|
|
5634
|
-
]
|
|
5635
|
-
};
|
|
6107
|
+
const env = sharedEnv();
|
|
6108
|
+
let pendingRaw = null;
|
|
6109
|
+
try {
|
|
6110
|
+
pendingRaw = await env.store.get(`${PENDING_ASK_PREFIX}${task_id}`);
|
|
6111
|
+
} catch {
|
|
6112
|
+
pendingRaw = null;
|
|
5636
6113
|
}
|
|
5637
|
-
|
|
5638
|
-
const pendingRaw = await ctx.env.CARRIER_USERS.get(pendingKey);
|
|
5639
|
-
if (pendingRaw === null || !await callerOwnsTask(ctx, task_id)) {
|
|
6114
|
+
if (pendingRaw === null || !await callerOwnsTask(ctx, task_id, runtime?.env.store)) {
|
|
5640
6115
|
return {
|
|
5641
6116
|
isError: true,
|
|
5642
6117
|
content: [
|
|
@@ -5650,75 +6125,42 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5650
6125
|
]
|
|
5651
6126
|
};
|
|
5652
6127
|
}
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
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
|
-
});
|
|
6128
|
+
const outcome = await resumeUiAgentTask(task_id, reply, env);
|
|
6129
|
+
ctx.audit({
|
|
6130
|
+
tool_name: "ui_agent_reply",
|
|
6131
|
+
ocs_method: "[ui-agent:ask-reply]",
|
|
6132
|
+
status: outcome.ok ? "ui_agent_resumed" : "error",
|
|
6133
|
+
dry_run: false,
|
|
6134
|
+
duration_ms: Date.now() - start,
|
|
6135
|
+
event_type: "ui_agent_resume"
|
|
6136
|
+
});
|
|
6137
|
+
if (!outcome.ok) {
|
|
5687
6138
|
return {
|
|
5688
6139
|
isError: true,
|
|
5689
6140
|
content: [
|
|
5690
6141
|
{
|
|
5691
6142
|
type: "text",
|
|
5692
6143
|
text: JSON.stringify({
|
|
5693
|
-
error: "
|
|
5694
|
-
message:
|
|
5695
|
-
code: result2.error?.code,
|
|
5696
|
-
task_id
|
|
6144
|
+
error: "resume_failed",
|
|
6145
|
+
message: outcome.error ?? "The task could not be resumed."
|
|
5697
6146
|
})
|
|
5698
6147
|
}
|
|
5699
6148
|
]
|
|
5700
6149
|
};
|
|
5701
6150
|
}
|
|
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
6151
|
return {
|
|
5713
6152
|
content: [
|
|
5714
6153
|
{
|
|
5715
6154
|
type: "text",
|
|
5716
|
-
text: JSON.stringify(
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
6155
|
+
text: JSON.stringify(
|
|
6156
|
+
{
|
|
6157
|
+
status: "ui_agent_resumed",
|
|
6158
|
+
task_id,
|
|
6159
|
+
reply_length: reply.length
|
|
6160
|
+
},
|
|
6161
|
+
null,
|
|
6162
|
+
2
|
|
6163
|
+
)
|
|
5722
6164
|
}
|
|
5723
6165
|
]
|
|
5724
6166
|
};
|
|
@@ -5728,7 +6170,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5728
6170
|
"ui_agent_list_pending",
|
|
5729
6171
|
{
|
|
5730
6172
|
title: "List Pending UI Agent Tasks (Waiting for Input)",
|
|
5731
|
-
description: "Returns
|
|
6173
|
+
description: "Returns browser automation tasks currently paused waiting for human input. Shows the agent's question, task URL, and when it was asked. Entries expire after 24 hours. Use ui_agent_reply to resume a task.",
|
|
5732
6174
|
inputSchema: {}
|
|
5733
6175
|
},
|
|
5734
6176
|
async () => {
|
|
@@ -5743,61 +6185,61 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5743
6185
|
]
|
|
5744
6186
|
};
|
|
5745
6187
|
}
|
|
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
|
-
};
|
|
6188
|
+
const kv = listableBinding(ctx);
|
|
6189
|
+
let taskIds;
|
|
6190
|
+
if (kv) {
|
|
6191
|
+
try {
|
|
6192
|
+
const listing = await kv.list({ prefix: PENDING_ASK_PREFIX });
|
|
6193
|
+
taskIds = listing.keys.map(
|
|
6194
|
+
(k) => k.name.slice(PENDING_ASK_PREFIX.length)
|
|
6195
|
+
);
|
|
6196
|
+
} catch (err7) {
|
|
6197
|
+
return {
|
|
6198
|
+
isError: true,
|
|
6199
|
+
content: [
|
|
6200
|
+
{
|
|
6201
|
+
type: "text",
|
|
6202
|
+
text: `Error listing pending tasks: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
6203
|
+
}
|
|
6204
|
+
]
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
} else {
|
|
6208
|
+
taskIds = [...runtime?.dispatchedTaskIds ?? []];
|
|
5774
6209
|
}
|
|
6210
|
+
const store = sharedEnv().store;
|
|
5775
6211
|
const entries = await Promise.all(
|
|
5776
|
-
|
|
5777
|
-
|
|
6212
|
+
taskIds.map(async (taskId) => {
|
|
6213
|
+
let raw;
|
|
6214
|
+
try {
|
|
6215
|
+
raw = await store.get(`${PENDING_ASK_PREFIX}${taskId}`);
|
|
6216
|
+
} catch {
|
|
6217
|
+
return null;
|
|
6218
|
+
}
|
|
5778
6219
|
if (!raw) return null;
|
|
6220
|
+
if (!await callerOwnsTask(ctx, taskId, runtime?.env.store)) {
|
|
6221
|
+
return null;
|
|
6222
|
+
}
|
|
5779
6223
|
try {
|
|
5780
6224
|
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
|
-
};
|
|
6225
|
+
return { ...parsed, expires_at: computeExpiresAt(parsed.asked_at) };
|
|
5787
6226
|
} catch {
|
|
5788
6227
|
return null;
|
|
5789
6228
|
}
|
|
5790
6229
|
})
|
|
5791
6230
|
);
|
|
5792
|
-
const validEntries = entries.filter(
|
|
6231
|
+
const validEntries = entries.filter(
|
|
6232
|
+
(e) => e !== null
|
|
6233
|
+
);
|
|
5793
6234
|
return {
|
|
5794
6235
|
content: [
|
|
5795
6236
|
{
|
|
5796
6237
|
type: "text",
|
|
5797
|
-
text: JSON.stringify(
|
|
5798
|
-
pending_tasks: validEntries,
|
|
5799
|
-
|
|
5800
|
-
|
|
6238
|
+
text: JSON.stringify(
|
|
6239
|
+
{ pending_tasks: validEntries, count: validEntries.length },
|
|
6240
|
+
null,
|
|
6241
|
+
2
|
|
6242
|
+
)
|
|
5801
6243
|
}
|
|
5802
6244
|
]
|
|
5803
6245
|
};
|
|
@@ -5807,277 +6249,6 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5807
6249
|
|
|
5808
6250
|
// src/tools-ui-agent-schedule.ts
|
|
5809
6251
|
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
6252
|
var UI_AGENT_SCHEDULE_TOOL_SCOPES = {
|
|
6082
6253
|
ui_agent_schedule_create: "admin",
|
|
6083
6254
|
ui_agent_schedule_list: "read",
|
|
@@ -6086,20 +6257,7 @@ var UI_AGENT_SCHEDULE_TOOL_SCOPES = {
|
|
|
6086
6257
|
ui_agent_schedule_resume: "admin",
|
|
6087
6258
|
ui_agent_usage: "read"
|
|
6088
6259
|
};
|
|
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
|
-
}
|
|
6260
|
+
var STDIO_NOTE = " Requires the remote Carrier MCP Worker deployment \u2014 a local CLI has no recurring runtime, so schedules cannot fire here.";
|
|
6103
6261
|
function scopeError(toolName, required, actual) {
|
|
6104
6262
|
return {
|
|
6105
6263
|
isError: true,
|
|
@@ -6111,427 +6269,126 @@ function scopeError(toolName, required, actual) {
|
|
|
6111
6269
|
]
|
|
6112
6270
|
};
|
|
6113
6271
|
}
|
|
6114
|
-
function
|
|
6272
|
+
function scopeDenied(ctx, toolName, required) {
|
|
6273
|
+
ctx.audit({
|
|
6274
|
+
tool_name: toolName,
|
|
6275
|
+
ocs_method: `[ui-agent:${toolName}]`,
|
|
6276
|
+
status: "scope_denied",
|
|
6277
|
+
dry_run: false,
|
|
6278
|
+
duration_ms: 0
|
|
6279
|
+
});
|
|
6280
|
+
return scopeError(toolName, required, ctx.props.scope);
|
|
6281
|
+
}
|
|
6282
|
+
function registerScheduleAndUsageTools(server2, ctx, runtime) {
|
|
6115
6283
|
server2.registerTool(
|
|
6116
6284
|
"ui_agent_schedule_create",
|
|
6117
6285
|
{
|
|
6118
|
-
title: "Create
|
|
6119
|
-
description: "Creates a recurring
|
|
6286
|
+
title: "Create UI Agent Schedule",
|
|
6287
|
+
description: "Creates a recurring browser-agent run on a cron schedule, to automate periodic OCS audits, fleet health checks, or any recurring browser-automation task. Minimum interval: 5 minutes. Requires admin scope." + STDIO_NOTE,
|
|
6120
6288
|
inputSchema: {
|
|
6121
6289
|
name: z6.string().describe("Human-readable name for this schedule"),
|
|
6122
6290
|
cron: z6.string().describe(
|
|
6123
6291
|
"Standard 5-field cron expression (minute hour day month weekday). Minimum interval: 5 minutes. Example: '0 */6 * * *' = every 6 hours."
|
|
6124
6292
|
),
|
|
6125
|
-
prompt_template: z6.string().describe(
|
|
6126
|
-
|
|
6293
|
+
prompt_template: z6.string().describe(
|
|
6294
|
+
"Agent prompt/task template the browser agent will execute on each run"
|
|
6295
|
+
),
|
|
6296
|
+
profile: z6.string().optional().describe(
|
|
6297
|
+
"Browser-agent profile to use. Defaults to the deployment's configured profile."
|
|
6298
|
+
)
|
|
6127
6299
|
}
|
|
6128
6300
|
},
|
|
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
|
-
}
|
|
6301
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_create", "admin") : uiAgentStubError("ui_agent_schedule_create", "recurring_runtime")
|
|
6207
6302
|
);
|
|
6208
6303
|
server2.registerTool(
|
|
6209
6304
|
"ui_agent_schedule_list",
|
|
6210
6305
|
{
|
|
6211
|
-
title: "List
|
|
6212
|
-
description: "Lists all
|
|
6213
|
-
inputSchema: {}
|
|
6214
|
-
},
|
|
6215
|
-
async () =>
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
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
|
-
}
|
|
6273
|
-
);
|
|
6274
|
-
server2.registerTool(
|
|
6275
|
-
"ui_agent_schedule_delete",
|
|
6276
|
-
{
|
|
6277
|
-
title: "Delete Manus Schedule",
|
|
6278
|
-
description: "Permanently deletes a Manus recurring schedule. This cannot be undone. Use ui_agent_schedule_pause to temporarily suspend instead. Requires admin scope.",
|
|
6279
|
-
inputSchema: {
|
|
6280
|
-
schedule_id: z6.string().describe("ID of the schedule to delete")
|
|
6281
|
-
}
|
|
6282
|
-
},
|
|
6283
|
-
async (args) => {
|
|
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 }] };
|
|
6306
|
+
title: "List UI Agent Schedules",
|
|
6307
|
+
description: "Lists all recurring browser-agent schedules configured for this deployment. Returns schedule IDs, names, cron expressions, status (active/paused), and next/last run timestamps." + STDIO_NOTE,
|
|
6308
|
+
inputSchema: {}
|
|
6309
|
+
},
|
|
6310
|
+
async () => !ctx.props.scope.includes("read") ? scopeDenied(ctx, "ui_agent_schedule_list", "read") : uiAgentStubError("ui_agent_schedule_list", "recurring_runtime")
|
|
6311
|
+
);
|
|
6312
|
+
server2.registerTool(
|
|
6313
|
+
"ui_agent_schedule_delete",
|
|
6314
|
+
{
|
|
6315
|
+
title: "Delete UI Agent Schedule",
|
|
6316
|
+
description: "Permanently deletes a recurring browser-agent schedule. This cannot be undone. Use ui_agent_schedule_pause to temporarily suspend instead. Requires admin scope." + STDIO_NOTE,
|
|
6317
|
+
inputSchema: {
|
|
6318
|
+
schedule_id: z6.string().describe("ID of the schedule to delete")
|
|
6341
6319
|
}
|
|
6342
|
-
}
|
|
6320
|
+
},
|
|
6321
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_delete", "admin") : uiAgentStubError("ui_agent_schedule_delete", "recurring_runtime")
|
|
6343
6322
|
);
|
|
6344
6323
|
server2.registerTool(
|
|
6345
6324
|
"ui_agent_schedule_pause",
|
|
6346
6325
|
{
|
|
6347
|
-
title: "Pause
|
|
6348
|
-
description: "Pauses an active
|
|
6326
|
+
title: "Pause UI Agent Schedule",
|
|
6327
|
+
description: "Pauses an active recurring browser-agent schedule. The schedule is preserved and can be resumed later with ui_agent_schedule_resume. Requires admin scope." + STDIO_NOTE,
|
|
6349
6328
|
inputSchema: {
|
|
6350
6329
|
schedule_id: z6.string().describe("ID of the schedule to pause")
|
|
6351
6330
|
}
|
|
6352
6331
|
},
|
|
6353
|
-
async (
|
|
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 }] };
|
|
6411
|
-
}
|
|
6412
|
-
}
|
|
6332
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_pause", "admin") : uiAgentStubError("ui_agent_schedule_pause", "recurring_runtime")
|
|
6413
6333
|
);
|
|
6414
6334
|
server2.registerTool(
|
|
6415
6335
|
"ui_agent_schedule_resume",
|
|
6416
6336
|
{
|
|
6417
|
-
title: "Resume
|
|
6418
|
-
description: "Resumes a paused
|
|
6337
|
+
title: "Resume UI Agent Schedule",
|
|
6338
|
+
description: "Resumes a paused recurring browser-agent schedule. Requires admin scope." + STDIO_NOTE,
|
|
6419
6339
|
inputSchema: {
|
|
6420
6340
|
schedule_id: z6.string().describe("ID of the schedule to resume")
|
|
6421
6341
|
}
|
|
6422
6342
|
},
|
|
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
|
-
}
|
|
6343
|
+
async () => !ctx.props.scope.includes("admin") ? scopeDenied(ctx, "ui_agent_schedule_resume", "admin") : uiAgentStubError("ui_agent_schedule_resume", "recurring_runtime")
|
|
6483
6344
|
);
|
|
6484
6345
|
server2.registerTool(
|
|
6485
6346
|
"ui_agent_usage",
|
|
6486
6347
|
{
|
|
6487
|
-
title: "
|
|
6488
|
-
description: "
|
|
6348
|
+
title: "UI Agent Usage & Spend",
|
|
6349
|
+
description: "Reports browser-agent spend for the current CLI session: runs dispatched, real vendor-billed cost in USD, and how many runs remain under the session limit. This is a per-process figure, not an account total \u2014 it resets when the CLI restarts, and it cannot see runs billed to the same API key by other processes or by the hosted Worker. Check your Browser Use account for the authoritative balance. Requires read scope.",
|
|
6489
6350
|
inputSchema: {}
|
|
6490
6351
|
},
|
|
6491
6352
|
async () => {
|
|
6492
|
-
const start = Date.now();
|
|
6493
6353
|
if (!ctx.props.scope.includes("read")) {
|
|
6494
6354
|
ctx.audit({
|
|
6495
6355
|
tool_name: "ui_agent_usage",
|
|
6496
|
-
ocs_method: "[
|
|
6356
|
+
ocs_method: "[ui-agent:usage]",
|
|
6497
6357
|
status: "scope_denied",
|
|
6498
6358
|
dry_run: false,
|
|
6499
6359
|
duration_ms: 0
|
|
6500
6360
|
});
|
|
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
6361
|
return {
|
|
6362
|
+
isError: true,
|
|
6517
6363
|
content: [
|
|
6518
6364
|
{
|
|
6519
6365
|
type: "text",
|
|
6520
|
-
text:
|
|
6366
|
+
text: `Scope denied: tool 'ui_agent_usage' requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
6521
6367
|
}
|
|
6522
6368
|
]
|
|
6523
6369
|
};
|
|
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
6370
|
}
|
|
6371
|
+
return {
|
|
6372
|
+
content: [
|
|
6373
|
+
{
|
|
6374
|
+
type: "text",
|
|
6375
|
+
text: JSON.stringify(
|
|
6376
|
+
{
|
|
6377
|
+
scope: "cli_session",
|
|
6378
|
+
configured: runtime.configured,
|
|
6379
|
+
runs_billed_this_session: runtime.spend.runs,
|
|
6380
|
+
cost_usd_this_session: runtime.spend.costUsd,
|
|
6381
|
+
runs_dispatched_this_session: runtime.budget.spent,
|
|
6382
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
6383
|
+
run_limit_env: MAX_RUNS_ENV,
|
|
6384
|
+
note: "Session-scoped. Resets when this CLI process exits and does not include runs billed to the same key elsewhere. The authoritative balance is in your Browser Use account."
|
|
6385
|
+
},
|
|
6386
|
+
null,
|
|
6387
|
+
2
|
|
6388
|
+
)
|
|
6389
|
+
}
|
|
6390
|
+
]
|
|
6391
|
+
};
|
|
6535
6392
|
}
|
|
6536
6393
|
);
|
|
6537
6394
|
}
|
|
@@ -12233,43 +12090,228 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12233
12090
|
|
|
12234
12091
|
// src/tools-ui-agent-generic.ts
|
|
12235
12092
|
import { z as z22 } from "zod";
|
|
12236
|
-
var
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12093
|
+
var COST_PER_STEP_USD = 0.014;
|
|
12094
|
+
var SESSION_OVERHEAD_USD = 5e-3;
|
|
12095
|
+
function estimateCostUsd2(steps) {
|
|
12096
|
+
return Math.round((COST_PER_STEP_USD * steps + SESSION_OVERHEAD_USD) * 100) / 100;
|
|
12097
|
+
}
|
|
12098
|
+
function notConfiguredError(toolName) {
|
|
12099
|
+
return {
|
|
12100
|
+
isError: true,
|
|
12101
|
+
content: [
|
|
12102
|
+
{
|
|
12103
|
+
type: "text",
|
|
12104
|
+
text: JSON.stringify({
|
|
12105
|
+
error: "browser_use_not_configured",
|
|
12106
|
+
tool: toolName,
|
|
12107
|
+
message: "BROWSER_USE_API_KEY is not set, so the browsing agent cannot run. Set it in the environment of the process running this MCP server. Runs are billed to that key at roughly $0.20-1.00 each."
|
|
12108
|
+
})
|
|
12109
|
+
}
|
|
12110
|
+
]
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
function scopeError2(toolName, required, actual) {
|
|
12114
|
+
return {
|
|
12115
|
+
isError: true,
|
|
12116
|
+
content: [
|
|
12117
|
+
{
|
|
12118
|
+
type: "text",
|
|
12119
|
+
text: JSON.stringify({
|
|
12120
|
+
error: "scope_denied",
|
|
12121
|
+
message: `Scope denied: tool '${toolName}' requires '${required}' scope. Your token has: [${actual.join(", ")}].`
|
|
12122
|
+
})
|
|
12123
|
+
}
|
|
12124
|
+
]
|
|
12125
|
+
};
|
|
12126
|
+
}
|
|
12127
|
+
function registerUiAgentGenericTools(server2, ctx, runtime) {
|
|
12250
12128
|
server2.registerTool(
|
|
12251
12129
|
"ui_agent_ask",
|
|
12252
12130
|
{
|
|
12253
|
-
title: "Dispatch Generic
|
|
12254
|
-
description: "
|
|
12131
|
+
title: "Dispatch Generic Browsing Agent",
|
|
12132
|
+
description: "Send a plain-language task to a hosted browsing agent, which drives a real browser on the open web \u2014 navigating pages, searching, extracting content, filling forms outside the OCS portal. Each run costs roughly $0.20-1.00 in model spend against your own BROWSER_USE_API_KEY, so treat it as the expensive last resort, not a lookup. Limited to CARRIER_BROWSER_USE_MAX_RUNS runs per CLI session (default 10). Params: `prompt` (1-4000 chars \u2014 name the target URL and the exact data or action), `max_steps` (1-30, default 15; prices the dry-run estimate only), `dry_run` (true returns only `estimated_cost_usd` and creates no session). Returns the finished result: unlike the hosted Worker, a local CLI has no background runtime, so this call blocks for the length of the run. Requires `write` scope. Do NOT use this to work out which Carrier tool answers a request \u2014 that is `carrier_ask`, which costs nothing. Do NOT use this for anything the OCS API already covers; every other tool here is faster, free, and audited.",
|
|
12255
12133
|
inputSchema: {
|
|
12256
|
-
prompt: z22.string().min(1).max(4e3).describe(
|
|
12257
|
-
|
|
12258
|
-
|
|
12134
|
+
prompt: z22.string().min(1).max(4e3).describe(
|
|
12135
|
+
"Natural-language task description for the browsing agent. Be specific: include target URLs, data to extract, or actions to perform."
|
|
12136
|
+
),
|
|
12137
|
+
max_steps: z22.number().int().min(1).max(30).optional().describe(
|
|
12138
|
+
"Advisory step budget (1\u201330, default 15). Prices the dry-run estimate; the hosted agent sets its own depth."
|
|
12139
|
+
),
|
|
12140
|
+
dry_run: z22.boolean().optional().describe(
|
|
12141
|
+
"Preview cost estimate without dispatching. Returns estimated_cost_usd only \u2014 no browser session is created."
|
|
12142
|
+
)
|
|
12259
12143
|
}
|
|
12260
12144
|
},
|
|
12261
|
-
async (
|
|
12145
|
+
async (args) => {
|
|
12146
|
+
const start = Date.now();
|
|
12147
|
+
const toolName = "ui_agent_ask";
|
|
12148
|
+
const { prompt, max_steps = 15, dry_run = false } = args;
|
|
12149
|
+
if (!ctx.props.scope.includes("write") && !ctx.props.scope.includes("admin")) {
|
|
12150
|
+
ctx.audit({
|
|
12151
|
+
tool_name: toolName,
|
|
12152
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12153
|
+
status: "scope_denied",
|
|
12154
|
+
dry_run: false,
|
|
12155
|
+
duration_ms: 0
|
|
12156
|
+
});
|
|
12157
|
+
return scopeError2(toolName, "write", ctx.props.scope);
|
|
12158
|
+
}
|
|
12159
|
+
if (dry_run) {
|
|
12160
|
+
return {
|
|
12161
|
+
content: [
|
|
12162
|
+
{
|
|
12163
|
+
type: "text",
|
|
12164
|
+
text: JSON.stringify(
|
|
12165
|
+
{
|
|
12166
|
+
status: "dry_run",
|
|
12167
|
+
estimated_cost_usd: estimateCostUsd2(max_steps),
|
|
12168
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
12169
|
+
note: "No browser session was created and nothing was billed."
|
|
12170
|
+
},
|
|
12171
|
+
null,
|
|
12172
|
+
2
|
|
12173
|
+
)
|
|
12174
|
+
}
|
|
12175
|
+
]
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
12179
|
+
return notConfiguredError(toolName);
|
|
12180
|
+
}
|
|
12181
|
+
try {
|
|
12182
|
+
runtime.budget.reserve();
|
|
12183
|
+
} catch (err7) {
|
|
12184
|
+
if (err7 instanceof SpendLimitError) {
|
|
12185
|
+
ctx.audit({
|
|
12186
|
+
tool_name: toolName,
|
|
12187
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12188
|
+
// The existing quota status, not a new one: AuditRow's status is a
|
|
12189
|
+
// closed union shared with the Worker, and a stdio-only value would
|
|
12190
|
+
// be a row shape the hosted surface never emits.
|
|
12191
|
+
status: "quota_exceeded",
|
|
12192
|
+
dry_run: false,
|
|
12193
|
+
duration_ms: 0,
|
|
12194
|
+
event_type: "ui_agent_dispatch"
|
|
12195
|
+
});
|
|
12196
|
+
return {
|
|
12197
|
+
isError: true,
|
|
12198
|
+
content: [
|
|
12199
|
+
{
|
|
12200
|
+
type: "text",
|
|
12201
|
+
text: JSON.stringify({
|
|
12202
|
+
error: "run_limit_reached",
|
|
12203
|
+
message: err7.message
|
|
12204
|
+
})
|
|
12205
|
+
}
|
|
12206
|
+
]
|
|
12207
|
+
};
|
|
12208
|
+
}
|
|
12209
|
+
throw err7;
|
|
12210
|
+
}
|
|
12211
|
+
try {
|
|
12212
|
+
const result2 = await runUiAgent(
|
|
12213
|
+
prompt,
|
|
12214
|
+
"Carrier CLI browsing agent",
|
|
12215
|
+
runtime.env
|
|
12216
|
+
);
|
|
12217
|
+
runtime.dispatchedTaskIds.add(result2.task_id);
|
|
12218
|
+
ctx.audit({
|
|
12219
|
+
tool_name: toolName,
|
|
12220
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12221
|
+
status: result2.status === "error" ? "error" : "ok",
|
|
12222
|
+
dry_run: false,
|
|
12223
|
+
duration_ms: Date.now() - start,
|
|
12224
|
+
event_type: "ui_agent_dispatch"
|
|
12225
|
+
});
|
|
12226
|
+
return {
|
|
12227
|
+
...result2.status === "error" ? { isError: true } : {},
|
|
12228
|
+
content: [
|
|
12229
|
+
{
|
|
12230
|
+
type: "text",
|
|
12231
|
+
text: JSON.stringify(
|
|
12232
|
+
{
|
|
12233
|
+
status: result2.status,
|
|
12234
|
+
task_id: result2.task_id,
|
|
12235
|
+
...result2.session_viewer_url ? { session_viewer_url: result2.session_viewer_url } : {},
|
|
12236
|
+
result: result2.result,
|
|
12237
|
+
session_spend_usd: runtime.spend.costUsd,
|
|
12238
|
+
runs_remaining_this_session: runtime.budget.remaining,
|
|
12239
|
+
note: "The run completed in-process. Task records live only for this CLI session \u2014 ui_agent_status can read this task_id until the process exits."
|
|
12240
|
+
},
|
|
12241
|
+
null,
|
|
12242
|
+
2
|
|
12243
|
+
)
|
|
12244
|
+
}
|
|
12245
|
+
]
|
|
12246
|
+
};
|
|
12247
|
+
} catch (err7) {
|
|
12248
|
+
ctx.audit({
|
|
12249
|
+
tool_name: toolName,
|
|
12250
|
+
ocs_method: "[ui-agent:generic-ask]",
|
|
12251
|
+
status: "error",
|
|
12252
|
+
dry_run: false,
|
|
12253
|
+
duration_ms: Date.now() - start,
|
|
12254
|
+
event_type: "ui_agent_dispatch"
|
|
12255
|
+
});
|
|
12256
|
+
return {
|
|
12257
|
+
isError: true,
|
|
12258
|
+
content: [
|
|
12259
|
+
{
|
|
12260
|
+
type: "text",
|
|
12261
|
+
text: JSON.stringify({
|
|
12262
|
+
error: "ui_agent_failed",
|
|
12263
|
+
message: err7 instanceof Error ? err7.message : String(err7)
|
|
12264
|
+
})
|
|
12265
|
+
}
|
|
12266
|
+
]
|
|
12267
|
+
};
|
|
12268
|
+
}
|
|
12269
|
+
}
|
|
12262
12270
|
);
|
|
12263
12271
|
server2.registerTool(
|
|
12264
12272
|
"ui_agent_status",
|
|
12265
12273
|
{
|
|
12266
|
-
title: "
|
|
12267
|
-
description: "
|
|
12274
|
+
title: "Poll Browsing Agent Task Status",
|
|
12275
|
+
description: "Returns the current state of a browsing-agent task by `task_id`, including its result once finished. In the local CLI, ui_agent_ask already returns the finished result, so this is mainly for re-reading a task within the same session. Task records do not survive the CLI process exiting. Requires `read` scope.",
|
|
12268
12276
|
inputSchema: {
|
|
12269
|
-
task_id: z22.string().describe("
|
|
12277
|
+
task_id: z22.string().describe("Task ID returned by ui_agent_ask")
|
|
12270
12278
|
}
|
|
12271
12279
|
},
|
|
12272
|
-
async (
|
|
12280
|
+
async (args) => {
|
|
12281
|
+
const toolName = "ui_agent_status";
|
|
12282
|
+
if (ctx.props.scope.length === 0) {
|
|
12283
|
+
return scopeError2(toolName, "read", ctx.props.scope);
|
|
12284
|
+
}
|
|
12285
|
+
if (!browserUseConfigured(runtime.env)) {
|
|
12286
|
+
return notConfiguredError(toolName);
|
|
12287
|
+
}
|
|
12288
|
+
const task = await getUiAgentTask(
|
|
12289
|
+
args.task_id,
|
|
12290
|
+
runtime.env
|
|
12291
|
+
);
|
|
12292
|
+
if (!task) {
|
|
12293
|
+
return {
|
|
12294
|
+
isError: true,
|
|
12295
|
+
content: [
|
|
12296
|
+
{
|
|
12297
|
+
type: "text",
|
|
12298
|
+
text: JSON.stringify({
|
|
12299
|
+
error: "task_not_found",
|
|
12300
|
+
message: `No task record for task_id=${args.task_id}. In the local CLI these live in memory for the lifetime of the process, so a task from an earlier session is gone rather than expired.`
|
|
12301
|
+
})
|
|
12302
|
+
}
|
|
12303
|
+
]
|
|
12304
|
+
};
|
|
12305
|
+
}
|
|
12306
|
+
return {
|
|
12307
|
+
content: [
|
|
12308
|
+
{
|
|
12309
|
+
type: "text",
|
|
12310
|
+
text: JSON.stringify(task, null, 2)
|
|
12311
|
+
}
|
|
12312
|
+
]
|
|
12313
|
+
};
|
|
12314
|
+
}
|
|
12273
12315
|
);
|
|
12274
12316
|
}
|
|
12275
12317
|
|
|
@@ -12295,8 +12337,15 @@ var stdioEnv = {
|
|
|
12295
12337
|
ASSETS: null,
|
|
12296
12338
|
CARRIER_USERS: null,
|
|
12297
12339
|
DOWNLOADS: null,
|
|
12298
|
-
MANUS_API_KEY: "",
|
|
12299
12340
|
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY ?? "",
|
|
12341
|
+
// Browser Use Cloud. Read from the environment with a real fallback, the way
|
|
12342
|
+
// STRIPE_SECRET_KEY above and the wallet values below are — unlike the
|
|
12343
|
+
// MANUS_API_KEY this replaces, which was hardcoded to "" with no fallback at
|
|
12344
|
+
// all, so every ui_agent_* tool returned a configuration error to every
|
|
12345
|
+
// caller from the day it shipped. Absent is still a valid state: the tools
|
|
12346
|
+
// stay registered and say so. Runs are billed to this key at ~$0.20-1.00
|
|
12347
|
+
// each, so dispatch is capped per process — see ui-agent-runtime.ts.
|
|
12348
|
+
BROWSER_USE_API_KEY: process.env.BROWSER_USE_API_KEY,
|
|
12300
12349
|
// Prepaid-wallet configuration. Read from the environment so a local operator
|
|
12301
12350
|
// who holds these values gets working wallet tools; when absent the tools stay
|
|
12302
12351
|
// registered (catalog parity with the Worker) and return an explicit
|
|
@@ -12340,13 +12389,16 @@ registerCountryHistoryTool(server, toolCtx);
|
|
|
12340
12389
|
registerDepletionEventsTool(server, toolCtx);
|
|
12341
12390
|
registerAllApps(server, toolCtx);
|
|
12342
12391
|
registerAllPricingTools(server, toolCtx);
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12392
|
+
var uiAgentRuntime = createUiAgentRuntime();
|
|
12393
|
+
var portalOptIn = readPortalOptIn();
|
|
12394
|
+
warnPortalOptIn(portalOptIn);
|
|
12395
|
+
registerScheduleAndUsageTools(server, toolCtx, uiAgentRuntime);
|
|
12396
|
+
registerAllUiAgentTools(server, toolCtx, uiAgentRuntime, portalOptIn);
|
|
12397
|
+
registerUiAgentAskTools(server, toolCtx, uiAgentRuntime);
|
|
12346
12398
|
registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
12347
12399
|
registerWalletTools(server, { env: stdioEnv, props });
|
|
12348
12400
|
registerGreenzoneTools(server, toolCtx);
|
|
12349
|
-
registerUiAgentGenericTools(server, toolCtx);
|
|
12401
|
+
registerUiAgentGenericTools(server, toolCtx, uiAgentRuntime);
|
|
12350
12402
|
registerStorefrontLogoTools(
|
|
12351
12403
|
server,
|
|
12352
12404
|
stdioEnv,
|