@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/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  usageOverPeriodParams,
48
48
  verifyStorefront,
49
49
  withOcsListSummary
50
- } from "./chunk-KHNJNJX3.js";
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/clerk.ts
4892
- import { createClerkClient } from "@clerk/backend";
4893
- async function getOcsPortalCredentials(env, orgId, userId) {
4894
- const baseUrl2 = "https://api.clerk.com/v1";
4895
- const headers = {
4896
- Authorization: `Bearer ${env.CLERK_SECRET_KEY}`,
4897
- "Content-Type": "application/json"
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: "ocs_portal_not_linked",
4960
- message: "OCS portal credentials are not configured. An organisation admin must link their OCS portal login via the Carrier Console settings page (Organisation Profile \u2192 OCS Portal) before UI-agent operations can be dispatched.",
4961
- action: "Navigate to https://console.carrier.llc/settings and link your OCS portal credentials."
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
- function noManusKeyError() {
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
- isError: true,
4970
- content: [
4971
- {
4972
- type: "text",
4973
- text: JSON.stringify({
4974
- error: "manus_api_not_configured",
4975
- message: "MANUS_API_KEY is not configured on this Carrier MCP deployment. UI-agent tools require a Manus API key to spawn browser automation agents. Contact your Carrier MCP administrator."
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
- function buildLoginPreamble(dashboardUrl) {
4982
- return `STEP 1 \u2014 LOGIN:
4983
- Navigate to ${dashboardUrl}/login (or the main page if no /login path).
4984
- Enter the OCS portal username and password provided below.
4985
- Wait for the dashboard to fully load after login.
4986
- If already logged in (session cookie persists), skip to STEP 2.
4987
-
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 UI_AGENT_RESULT_SCHEMA = {
4991
- type: "object",
4992
- properties: {
4993
- success: { type: "boolean", description: "Whether the operation completed successfully" },
4994
- summary: { type: "string", description: "Human-readable summary of what was done" },
4995
- entity_id: { type: "string", description: "ID of the created/modified entity (if applicable)" },
4996
- error_message: { type: "string", description: "Error description if the operation failed" },
4997
- screenshots_taken: { type: "number", description: "Number of screenshots captured during the operation" }
4998
- },
4999
- required: ["success", "summary"]
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 wrapUiAgentHandler(toolName, gapId, requiredScope, ctx, buildPrompt) {
5002
- return async (args) => {
5003
- const start = Date.now();
5004
- const isDryRun = args.dry_run === true;
5005
- if (!ctx.props.scope.includes(requiredScope)) {
5006
- ctx.audit({
5007
- tool_name: toolName,
5008
- ocs_method: `[ui-agent:${gapId}]`,
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
- if (!ctx.env.MANUS_API_KEY) {
5024
- return noManusKeyError();
5025
- }
5026
- const clerkUserId = ctx.props.sub.startsWith("clerk_") ? ctx.props.sub.slice(6) : void 0;
5027
- const creds = await getOcsPortalCredentials(ctx.env, ctx.props.org_id, clerkUserId);
5028
- if (!creds) {
5029
- return noCredentialsError();
5030
- }
5031
- const dashboardUrl = ctx.env.OCS_DASHBOARD_URL ?? OCS_DASHBOARD_DEFAULT;
5032
- const prompt = buildPrompt(args, dashboardUrl);
5033
- const fullPrompt = `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.
5034
-
5035
- OCS PORTAL CREDENTIALS (use these to log in \u2014 NEVER include them in your output):
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
- STEP 3 \u2014 VERIFICATION:
5043
- After completing the operation, verify the result by checking the dashboard shows the expected state.
5044
- Take a screenshot of the final state for audit purposes.
5045
- Report success or failure with a clear summary.`;
5046
- if (isDryRun) {
5047
- const redactedPrompt = fullPrompt.replaceAll(creds.password, "***REDACTED***").replaceAll(creds.username, "***REDACTED***");
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: `[ui-agent:${gapId}]`,
5051
- status: "dry_run",
5052
- dry_run: true,
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: JSON.stringify({
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 createManusTask(
5073
- ctx.env.MANUS_API_KEY,
5642
+ const result2 = await runUiAgent(
5074
5643
  fullPrompt,
5075
- `Carrier MCP UI Agent: ${toolName} (${gapId})`,
5076
- UI_AGENT_RESULT_SCHEMA
5644
+ `Carrier CLI UI Agent: ${toolName} (${gapId})`,
5645
+ runtime.env,
5646
+ { ownerSub: ctx.props.sub, redact }
5077
5647
  );
5078
- if (!result2.ok || !result2.task_id) {
5648
+ if (!result2.task_id) {
5079
5649
  ctx.audit({
5080
5650
  tool_name: toolName,
5081
- ocs_method: `[ui-agent:${gapId}]`,
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
- isError: true,
5089
- content: [
5090
- {
5091
- type: "text",
5092
- text: JSON.stringify({
5093
- error: "manus_dispatch_failed",
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: `[ui-agent:${gapId}]`,
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: `[ui-agent:${gapId}]`,
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: `Error dispatching UI agent: ${err7 instanceof Error ? err7.message : String(err7)}`
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
- var UI_AGENT_TOOL_SCOPES = {
5149
- ui_create_steering_list: "write",
5150
- ui_build_steering_list: "write",
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 Manus browser agent will be dispatched to perform the operation. Params: `name` (steering list name), `description` (optional). Returns: dispatch confirmation with Manus task ID for tracking.",
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
- wrapUiAgentHandler(
5739
+ gated(
5174
5740
  "ui_create_steering_list",
5175
5741
  "G-01",
5176
5742
  "write",
5177
- ctx,
5178
- (args, dashboardUrl) => `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
5179
- Create a new steering list with the following details:
5180
- Name: ${args.name}
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
- wrapUiAgentHandler(
5762
+ gated(
5201
5763
  "ui_build_steering_list",
5202
5764
  "G-02",
5203
5765
  "write",
5204
- ctx,
5205
- (args, dashboardUrl) => `Navigate to the Steering Lists section of the OCS dashboard at ${dashboardUrl}.
5206
- Open steering list ID ${args.steering_list_id} for editing.
5207
- Operator type: ${args.operator_type ?? "priority"}
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
- wrapUiAgentHandler(
5783
+ gated(
5226
5784
  "ui_set_account_steering_list",
5227
5785
  "G-03",
5228
5786
  "write",
5229
- ctx,
5230
- (args, dashboardUrl) => `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
5231
- Open account ID ${args.account_id}.
5232
- ` + (args.steering_list_id === 0 ? `Remove/unset the steering list assignment from this account.
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: [{ type: "text", text: "Scope denied: tool 'ui_request_reseller_relay_change' requires 'write' scope." }]
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: [{ type: "text", text: JSON.stringify({ error: "relay_state_required", message: "Choose at least one relay flag and desired state." }) }]
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: [{ type: "text", text: JSON.stringify({
5288
- status: "parent_request_drafted",
5289
- capability: "parent_controlled",
5290
- reseller_id: resellerId,
5291
- parent_reseller: resellerId === 1170 ? "Bridge4IP" : "parent reseller/support",
5292
- draft,
5293
- sent: false
5294
- }, null, 2) }]
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
- wrapUiAgentHandler(
5887
+ gated(
5311
5888
  "ui_create_account",
5312
5889
  "G-05",
5313
5890
  "admin",
5314
- ctx,
5315
- (args, dashboardUrl) => `Navigate to the Accounts section of the OCS dashboard at ${dashboardUrl}.
5316
- Create a new account with the following details:
5317
- Name: ${args.name}
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
- wrapUiAgentHandler(
5909
+ gated(
5338
5910
  "ui_create_destination_list",
5339
5911
  "G-12",
5340
5912
  "write",
5341
- ctx,
5342
- (args, dashboardUrl) => `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
5343
- Create a new destination list with the following details:
5344
- Name: ${args.name}
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
- wrapUiAgentHandler(
5932
+ gated(
5365
5933
  "ui_edit_destination_list",
5366
5934
  "G-12",
5367
5935
  "write",
5368
- ctx,
5369
- (args, dashboardUrl) => `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
5370
- Open destination list ID ${args.destination_list_id} for editing.
5371
- ` + (args.new_name ? `Rename to: ${args.new_name}
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
- wrapUiAgentHandler(
5952
+ gated(
5389
5953
  "ui_delete_destination_list",
5390
5954
  "G-12",
5391
5955
  "admin",
5392
- ctx,
5393
- (args, dashboardUrl) => `Navigate to the Destination Lists section of the OCS dashboard at ${dashboardUrl}.
5394
- Find destination list ID ${args.destination_list_id}.
5395
- Delete this destination list. Confirm the deletion when prompted.
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
- wrapUiAgentHandler(
5972
+ gated(
5411
5973
  "ui_delete_package_template",
5412
5974
  "G-18",
5413
5975
  "admin",
5414
- ctx,
5415
- (args, dashboardUrl) => `Navigate to the Package Templates section of the OCS dashboard at ${dashboardUrl}.
5416
- Find package template ID ${args.template_id}.
5417
- Delete this package template. Confirm the deletion when prompted.
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
- wrapUiAgentHandler(
5995
+ gated(
5436
5996
  "ui_edit_location_zone",
5437
5997
  "G-19",
5438
5998
  "write",
5439
- ctx,
5440
- (args, dashboardUrl) => `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
5441
- Open location zone ID ${args.zone_id} for editing.
5442
- ` + (args.new_name ? `Rename to: ${args.new_name}
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
- wrapUiAgentHandler(
6015
+ gated(
5460
6016
  "ui_delete_location_zone",
5461
6017
  "G-19",
5462
6018
  "admin",
5463
- ctx,
5464
- (args, dashboardUrl) => `Navigate to the Location Zones section of the OCS dashboard at ${dashboardUrl}.
5465
- Find location zone ID ${args.zone_id}.
5466
- Delete this location zone. Confirm the deletion when prompted.
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
- async function callerOwnsTask(ctx, taskId) {
5576
- let raw;
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(`steel_task:${taskId}`);
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 registerUiAgentAskTools(server2, ctx) {
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 Manus browser automation task that paused with stop_reason 'ask'. 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.",
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("Manus task ID to resume (from ui_agent_list_pending)"),
5598
- reply: z5.string().describe("Your answer to the agent's question (e.g. a 2FA code or confirmation)")
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 keys = getManusKeys(ctx.env);
5623
- if (!keys) {
5624
- return {
5625
- isError: true,
5626
- content: [
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
- const pendingKey = `${PENDING_ASK_PREFIX}${task_id}`;
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
- let result2;
5654
- try {
5655
- const replyOutcome = await askReply(keys, task_id, reply);
5656
- result2 = replyOutcome.data;
5657
- } catch (err7) {
5658
- ctx.audit({
5659
- tool_name: "ui_agent_reply",
5660
- ocs_method: "[ui-agent:ask-reply]",
5661
- status: "error",
5662
- dry_run: false,
5663
- duration_ms: Date.now() - start,
5664
- event_type: "ui_agent_resume",
5665
- manus_task_id: task_id
5666
- });
5667
- return {
5668
- isError: true,
5669
- content: [
5670
- {
5671
- type: "text",
5672
- text: `Error calling Manus task.reply: ${err7 instanceof Error ? err7.message : String(err7)}`
5673
- }
5674
- ]
5675
- };
5676
- }
5677
- if (!result2.ok) {
5678
- ctx.audit({
5679
- tool_name: "ui_agent_reply",
5680
- ocs_method: "[ui-agent:ask-reply]",
5681
- status: "error",
5682
- dry_run: false,
5683
- duration_ms: Date.now() - start,
5684
- event_type: "ui_agent_resume",
5685
- manus_task_id: task_id
5686
- });
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: "manus_reply_failed",
5694
- message: result2.error?.message ?? "Manus task.reply returned ok=false",
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
- status: "resumed",
5718
- task_id,
5719
- reply_length: reply.length,
5720
- message: "The Manus agent has received your reply and is continuing the task. The task will emit a task_stopped webhook when complete."
5721
- }, null, 2)
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 all Manus browser automation tasks that are currently paused waiting for human input (stop_reason 'ask'). 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.",
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
- let keys;
5747
- try {
5748
- const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
5749
- keys = listing.keys;
5750
- } catch (err7) {
5751
- return {
5752
- isError: true,
5753
- content: [
5754
- {
5755
- type: "text",
5756
- text: `Error listing pending tasks: ${err7 instanceof Error ? err7.message : String(err7)}`
5757
- }
5758
- ]
5759
- };
5760
- }
5761
- if (keys.length === 0) {
5762
- return {
5763
- content: [
5764
- {
5765
- type: "text",
5766
- text: JSON.stringify({
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
- keys.map(async ({ name }) => {
5777
- const raw = await ctx.env.CARRIER_USERS.get(name);
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
- const taskId = parsed.task_id ?? name.slice(PENDING_ASK_PREFIX.length);
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((e) => e !== null);
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
- count: validEntries.length
5800
- }, null, 2)
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
- function noManusKeyError2() {
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 registerScheduleAndUsageTools(server2, ctx) {
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 Manus Schedule (UI Agent)",
6119
- description: "Creates a recurring Manus agent run on a cron schedule. Use this to automate periodic OCS audits, fleet health checks, or any recurring browser-automation task. Minimum interval: 5 minutes (*/1, */2, */3, */4 and '* * * * *' are rejected). Requires admin scope. Returns schedule_id.",
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("Agent prompt/task template the Manus agent will execute on each run"),
6126
- profile: z6.string().optional().describe("Manus agent profile to use. Defaults to 'manus-1.6-lite'.")
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 (args) => {
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 Manus Schedules",
6212
- description: "Lists all Manus recurring schedules configured for this API key. Returns schedule IDs, names, cron expressions, status (active/paused), and next/last run timestamps.",
6213
- inputSchema: {}
6214
- },
6215
- async () => {
6216
- const start = Date.now();
6217
- if (!ctx.props.scope.includes("read")) {
6218
- ctx.audit({
6219
- tool_name: "ui_agent_schedule_list",
6220
- ocs_method: "[manus:schedule.list]",
6221
- status: "scope_denied",
6222
- dry_run: false,
6223
- duration_ms: 0
6224
- });
6225
- return scopeError("ui_agent_schedule_list", "read", ctx.props.scope);
6226
- }
6227
- if (!ctx.env.MANUS_API_KEY) {
6228
- return noManusKeyError2();
6229
- }
6230
- try {
6231
- const result2 = await listSchedules(ctx.env.MANUS_API_KEY);
6232
- ctx.audit({
6233
- tool_name: "ui_agent_schedule_list",
6234
- ocs_method: "[manus:schedule.list]",
6235
- status: result2.ok ? "ok" : "error",
6236
- dry_run: false,
6237
- duration_ms: Date.now() - start
6238
- });
6239
- if (!result2.ok) {
6240
- return {
6241
- isError: true,
6242
- content: [
6243
- {
6244
- type: "text",
6245
- text: JSON.stringify({
6246
- error: "schedule_list_failed",
6247
- message: result2.error?.message ?? "Manus schedule.list returned ok=false"
6248
- })
6249
- }
6250
- ]
6251
- };
6252
- }
6253
- return {
6254
- content: [
6255
- {
6256
- type: "text",
6257
- text: JSON.stringify({ schedules: result2.schedules ?? [] }, null, 2)
6258
- }
6259
- ]
6260
- };
6261
- } catch (err7) {
6262
- ctx.audit({
6263
- tool_name: "ui_agent_schedule_list",
6264
- ocs_method: "[manus:schedule.list]",
6265
- status: "error",
6266
- dry_run: false,
6267
- duration_ms: Date.now() - start
6268
- });
6269
- const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
6270
- return { isError: true, content: [{ type: "text", text: msg }] };
6271
- }
6272
- }
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 Manus Schedule",
6348
- description: "Pauses an active Manus recurring schedule. The schedule is preserved and can be resumed later with ui_agent_schedule_resume. Requires admin scope.",
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 (args) => {
6354
- const start = Date.now();
6355
- if (!ctx.props.scope.includes("admin")) {
6356
- ctx.audit({
6357
- tool_name: "ui_agent_schedule_pause",
6358
- ocs_method: "[manus:schedule.pause]",
6359
- status: "scope_denied",
6360
- dry_run: false,
6361
- duration_ms: 0
6362
- });
6363
- return scopeError("ui_agent_schedule_pause", "admin", ctx.props.scope);
6364
- }
6365
- if (!ctx.env.MANUS_API_KEY) {
6366
- return noManusKeyError2();
6367
- }
6368
- try {
6369
- const result2 = await pauseSchedule(ctx.env.MANUS_API_KEY, args.schedule_id);
6370
- ctx.audit({
6371
- tool_name: "ui_agent_schedule_pause",
6372
- ocs_method: "[manus:schedule.pause]",
6373
- status: result2.ok ? "ok" : "error",
6374
- dry_run: false,
6375
- duration_ms: Date.now() - start,
6376
- event_type: "ui_agent_dispatch"
6377
- });
6378
- if (!result2.ok) {
6379
- return {
6380
- isError: true,
6381
- content: [
6382
- {
6383
- type: "text",
6384
- text: JSON.stringify({
6385
- error: "schedule_pause_failed",
6386
- message: result2.error?.message ?? "Manus schedule.pause returned ok=false"
6387
- })
6388
- }
6389
- ]
6390
- };
6391
- }
6392
- return {
6393
- content: [
6394
- {
6395
- type: "text",
6396
- text: JSON.stringify({ status: "paused", schedule_id: args.schedule_id }, null, 2)
6397
- }
6398
- ]
6399
- };
6400
- } catch (err7) {
6401
- ctx.audit({
6402
- tool_name: "ui_agent_schedule_pause",
6403
- ocs_method: "[manus:schedule.pause]",
6404
- status: "error",
6405
- dry_run: false,
6406
- duration_ms: Date.now() - start,
6407
- event_type: "ui_agent_dispatch"
6408
- });
6409
- const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
6410
- return { isError: true, content: [{ type: "text", text: msg }] };
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 Manus Schedule",
6418
- description: "Resumes a paused Manus recurring schedule. Requires admin scope.",
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 (args) => {
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: "Manus Usage & Credits",
6488
- description: "Returns current month Manus credit spend, remaining balance, and task count. Result is cached for 60 seconds in KV to avoid rate-limiting the Manus API. Emits an Analytics Engine event when remaining_credits drops below 1000 (warning) or 100 (critical). Use this to track Carrier's Manus credit burn.",
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: "[manus:usage.get]",
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: JSON.stringify({ ...data, from_cache }, null, 2)
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 STDIO_ERROR = {
12237
- isError: true,
12238
- content: [
12239
- {
12240
- type: "text",
12241
- text: JSON.stringify({
12242
- error: "requires_worker_runtime",
12243
- message: "ui_agent_ask and ui_agent_status require the remote Carrier MCP Worker deployment (mcp.carrier.llc/mcp) \u2014 they cannot run in stdio/local mode because Steel browser sessions and KV task state require Cloudflare Workers runtime bindings. Connect to the remote MCP endpoint to use these tools.",
12244
- remote_url: "https://mcp.carrier.llc/mcp"
12245
- })
12246
- }
12247
- ]
12248
- };
12249
- function registerUiAgentGenericTools(server2, _ctx) {
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 Steel Browsing Agent",
12254
- description: "Dispatch a Steel browsing agent task with a natural-language prompt. The agent will navigate the web, use Tavily web search if needed, and return the result. Use for ad-hoc research, page extraction, form filling outside the OCS portal. Pro/Enterprise scope required. Cost ~$0.20-1.00 per task depending on complexity. Returns task_id immediately \u2014 poll with ui_agent_status for completion. Set dry_run=true to preview cost estimate without executing. NOTE: requires remote Worker deployment (mcp.carrier.llc/mcp) \u2014 not available in stdio mode.",
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("Natural-language task description for the Steel browsing agent."),
12257
- max_steps: z22.number().int().min(1).max(30).optional().describe("Maximum agent steps (1\u201330, default 15)."),
12258
- dry_run: z22.boolean().optional().describe("Preview cost estimate without dispatching.")
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 (_args) => STDIO_ERROR
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: "Get Steel Agent Task Status",
12267
- description: "Poll the status of a Steel browser agent task dispatched by ui_agent_ask or any ui_* tool. Returns current status (pending | running | completed | failed) and result when available. NOTE: requires remote Worker deployment (mcp.carrier.llc/mcp) \u2014 not available in stdio mode.",
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("Steel task ID returned by ui_agent_ask or any ui_* tool dispatch")
12277
+ task_id: z22.string().describe("Task ID returned by ui_agent_ask")
12270
12278
  }
12271
12279
  },
12272
- async (_args) => STDIO_ERROR
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
- registerScheduleAndUsageTools(server, toolCtx);
12344
- registerAllUiAgentTools(server, toolCtx);
12345
- registerUiAgentAskTools(server, toolCtx);
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,