@carrierllc/mcp 0.9.3 → 0.10.1

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