@carrierllc/mcp 0.9.2 → 0.10.0

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