@mono-agent/operator-adapter 0.20.11 → 0.21.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.
@@ -1,14 +1,42 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { isAbsolute } from "node:path";
4
- import { AGENT_LIVE_INPUT_MAX_CHARACTERS, DEFAULT_AGENT_ATTACHMENT_MAX_BYTES, DEFAULT_AGENT_ATTACHMENT_MIME_ALLOWLIST, MCP_APP_RESOURCE_MIME_TYPE, MCP_APP_SUPPORTED_VERSIONS, MAX_AGENT_REPLY_PARTS, BoundedHttpResponseWriter, agentAttachmentKindFromMimeType, closeServerBounded, createChannelUserCancelReason, decodeAgentAttachmentText, isAgentResponseCancelledError, parseProcessJobProjection, parseProcessJobProjections, serializeAgentStreamFrame, } from "@mono-agent/agent-contracts";
5
- import { assertSafeBind, bearerTokensEqual, hostForUrl, isLoopbackHost, listen, normalizeOptionalString, parseCronOperatorOverview, parseCronOperatorRunDetail, parseCronOperatorRunPage, readAuthorizationBearer, } from "@mono-agent/agent-contracts";
4
+ import { AGENT_LIVE_INPUT_MAX_CHARACTERS, AGENT_CONTEXT_IMPORT_MAX_CONVERSATION_ID_BYTES, AGENT_CONTEXT_IMPORT_MAX_IDEMPOTENCY_KEY_BYTES, AGENT_CONTEXT_IMPORT_MAX_TEXT_BYTES, AGENT_CONTEXT_IMPORT_VERSION, DEFAULT_AGENT_ATTACHMENT_MAX_BYTES, DEFAULT_AGENT_ATTACHMENT_MIME_ALLOWLIST, MCP_APP_RESOURCE_MIME_TYPE, MCP_APP_SUPPORTED_VERSIONS, MAX_AGENT_REPLY_PARTS, MAX_PROVIDER_AUTH_BODY_BYTES, ProviderAuthOperationError, BoundedHttpResponseWriter, agentAttachmentKindFromMimeType, closeServerBounded, createChannelUserCancelReason, decodeAgentAttachmentText, isAgentResponseCancelledError, parseMonitorProjection, parseMonitorProjections, parseProcessJobProjection, parseProcessJobProjections, parseProviderAuthSessionInput, parseProviderAuthSessionSnapshot, parseProviderAuthSessionStartInput, parseProviderAuthCheckSessionSnapshot, parseProviderAuthCheckStartInput, parseProviderAuthStatusSnapshot, serializeAgentStreamFrame, } from "@mono-agent/agent-contracts";
5
+ import { assertSafeBind, bearerTokensEqual, hostForUrl, isLoopbackHost, listen, MAX_INFO_BODY_BYTES, normalizeOptionalString, parseCronOperatorOverview, parseCronOperatorRunDetail, parseCronOperatorRunPage, readAuthorizationBearer, } from "@mono-agent/agent-contracts";
6
6
  import express, {} from "express";
7
+ const TARGET_WAITER_TIMEOUT_MS = 10 * 60 * 1_000;
8
+ const MAX_TARGET_WAITERS_PER_OPERATION = 100;
9
+ const MAX_TARGET_WAITERS_GLOBAL = 1_000;
10
+ function liveInputTargetKey(conversationId, turnId) {
11
+ return `${conversationId.length}:${conversationId}${turnId}`;
12
+ }
13
+ function settleLiveInputOffer(res, offer) {
14
+ if (offer.status === "unavailable") {
15
+ res.status(200).json(offer);
16
+ return;
17
+ }
18
+ void offer.settled.then((settlement) => {
19
+ if (!res.writableEnded)
20
+ res.status(200).json(settlement);
21
+ }).catch(() => {
22
+ if (!res.writableEnded)
23
+ res.status(200).json({ status: "uncertain", reason: "delivery_uncertain" });
24
+ });
25
+ }
7
26
  import { DEFAULT_BASE_PATH, DEFAULT_HOST, DEFAULT_PORT, MAX_FRAME_BYTES, TUI_WIRE_SCHEMA } from "./constants.js";
8
27
  import { CronOperatorError, MAX_CRON_OPERATOR_RESPONSE_BYTES, MAX_CRON_OPERATOR_RUN_PAGE, } from "./cron.js";
9
28
  import { TuiAdapterError } from "./errors.js";
10
29
  const MAX_TURN_BODY_BYTES = 96 * 1024 * 1024;
30
+ const MAX_MODEL_CATALOG_PAGE_SIZE = 200;
31
+ const DEFAULT_MODEL_CATALOG_PAGE_SIZE = 100;
32
+ const MAX_MODEL_CATALOG_RESPONSE_BYTES = 1024 * 1024;
33
+ const MAX_MODEL_CATALOG_PROVIDER_BYTES = 256;
34
+ const MAX_MODEL_CATALOG_QUERY_BYTES = 512;
35
+ const MAX_MODEL_CATALOG_CURSOR_BYTES = 4 * 1024;
11
36
  const MAX_VERBATIM_BODY_BYTES = 2 * 1024 * 1024;
37
+ // `{"idempotencyKey":"","text":""}` is 31 bytes. Each legal decoded
38
+ // text/key byte can require a six-byte JSON escape; 31 + 6*(32768+512).
39
+ const MAX_CONTEXT_IMPORT_BODY_BYTES = 199_711;
12
40
  const MAX_VERBATIM_TEXT_CHARACTERS = 200_000;
13
41
  const MAX_VERBATIM_TEXT_BYTES = 1024 * 1024;
14
42
  const MAX_LIVE_INPUT_BODY_BYTES = 32 * 1024;
@@ -38,6 +66,10 @@ export async function startTuiAdapter(options) {
38
66
  if ((options.processJobs === undefined) !== (processJobsBearer === undefined)) {
39
67
  throw new TuiAdapterError("invalid_config", "processJobs and processJobsBearer must be configured together.");
40
68
  }
69
+ const monitorsBearer = normalizeOptionalString(options.monitorsBearer);
70
+ if ((options.monitors === undefined) !== (monitorsBearer === undefined)) {
71
+ throw new TuiAdapterError("invalid_config", "monitors and monitorsBearer must be configured together.");
72
+ }
41
73
  if (options.requestToolEnvironment !== undefined && !isLoopbackHost(host)) {
42
74
  throw new TuiAdapterError("unsafe_host", "Request tool environment requires a loopback-only TUI adapter bind.", { host });
43
75
  }
@@ -45,12 +77,25 @@ export async function startTuiAdapter(options) {
45
77
  const app = express();
46
78
  const server = createServer(app);
47
79
  const activeTurns = new Set();
80
+ const liveInputTargets = new Map();
81
+ let pendingTargetWaiters = 0;
82
+ const settleTarget = (target, runId) => {
83
+ target.state = runId === undefined ? "closed" : "ready";
84
+ if (runId === undefined)
85
+ delete target.runId;
86
+ else
87
+ target.runId = runId;
88
+ for (const waiter of [...target.waiters])
89
+ waiter(runId);
90
+ };
48
91
  let stopping = false;
49
92
  let stopPromise;
50
93
  const infoPath = `${basePath}/v1/info`;
94
+ const modelsPath = `${basePath}/v1/models`;
51
95
  const turnsPath = `${basePath}/v1/turns`;
52
96
  const cancelPath = `${basePath}/v1/conversations/:conversationId/cancel`;
53
97
  const verbatimPath = `${basePath}/v1/conversations/:conversationId/verbatim`;
98
+ const contextImportPath = `${basePath}/v1/conversations/:conversationId/context-imports`;
54
99
  const liveInputPath = `${basePath}/v1/conversations/:conversationId/live-input`;
55
100
  const replyArtifactPath = `${basePath}/v1/conversations/:conversationId/reply-artifacts/:artifactId`;
56
101
  const mcpAppPath = `${basePath}/v1/conversations/:conversationId/mcp-apps/:invocationId`;
@@ -66,6 +111,15 @@ export async function startTuiAdapter(options) {
66
111
  const jobsPath = `${basePath}/v1/jobs`;
67
112
  const jobPath = `${basePath}/v1/jobs/:jobId`;
68
113
  const jobCancelPath = `${basePath}/v1/jobs/:jobId/cancel`;
114
+ const monitorsPath = `${basePath}/v1/monitors`;
115
+ const monitorPath = `${basePath}/v1/monitors/:monitorId`;
116
+ const monitorCancelPath = `${basePath}/v1/monitors/:monitorId/cancel`;
117
+ const providerAuthPath = `${basePath}/v1/provider-auth`;
118
+ const providerAuthSessionsPath = `${providerAuthPath}/sessions`;
119
+ const providerAuthSessionPath = `${providerAuthSessionsPath}/:sessionId`;
120
+ const providerAuthInputPath = `${providerAuthSessionPath}/input`;
121
+ const providerAuthChecksPath = `${providerAuthPath}/checks`;
122
+ const providerAuthCheckPath = `${providerAuthChecksPath}/:checkId`;
69
123
  app.get(infoPath, (req, res) => {
70
124
  if (!authorize(req, res, apiKey)) {
71
125
  return;
@@ -86,7 +140,7 @@ export async function startTuiAdapter(options) {
86
140
  });
87
141
  void Promise.all([resolveInfo(options.info), cronInfo])
88
142
  .then(([info, cronState]) => {
89
- res.status(200).json({
143
+ sendBoundedInfo(res, {
90
144
  schema: TUI_WIRE_SCHEMA,
91
145
  pid: process.pid,
92
146
  capabilities: {
@@ -105,7 +159,14 @@ export async function startTuiAdapter(options) {
105
159
  }
106
160
  : {}),
107
161
  ...(typeof options.responder.offerLiveInput === "function" ? { liveInput: true } : {}),
162
+ ...(typeof options.responder.offerLiveInput === "function"
163
+ && options.responder.liveInputOwnership?.version === 1
164
+ ? { liveInputTargeting: { version: 1 } }
165
+ : {}),
108
166
  ...(typeof options.responder.deliverVerbatim === "function" ? { historyAppend: true } : {}),
167
+ ...(typeof options.responder.importContext === "function"
168
+ ? { contextImport: { version: AGENT_CONTEXT_IMPORT_VERSION, maxTextBytes: AGENT_CONTEXT_IMPORT_MAX_TEXT_BYTES } }
169
+ : {}),
109
170
  ...(options.interaction === undefined ? {} : { askUser: true }),
110
171
  ...(typeof options.interaction?.getAsk === "function" ? { askById: true } : {}),
111
172
  ...(cronState.kind === "absent"
@@ -122,7 +183,19 @@ export async function startTuiAdapter(options) {
122
183
  },
123
184
  }),
124
185
  ...(options.processJobs === undefined || processJobsBearer === undefined ? {} : { jobs: true }),
186
+ ...(options.monitors === undefined || monitorsBearer === undefined ? {} : { monitors: true }),
125
187
  ...(options.requestToolEnvironment === undefined ? {} : { toolEnvironment: true }),
188
+ ...(options.modelCatalog === undefined
189
+ ? {}
190
+ : { modelCatalog: { version: 1, maxPageSize: MAX_MODEL_CATALOG_PAGE_SIZE } }),
191
+ ...(options.providerAuth === undefined
192
+ ? {}
193
+ : {
194
+ providerAuth: {
195
+ version: 1,
196
+ ...(options.providerAuth.checks === undefined ? {} : { checks: { version: 1 } }),
197
+ },
198
+ }),
126
199
  },
127
200
  ...(info?.label === undefined ? {} : { label: info.label }),
128
201
  ...(info?.model === undefined ? {} : { model: info.model }),
@@ -132,13 +205,48 @@ export async function startTuiAdapter(options) {
132
205
  ? {}
133
206
  : { modelOptions: info.modelOptions }),
134
207
  ...(info?.skills === undefined ? {} : { skills: info.skills }),
135
- });
208
+ ...(info?.providers === undefined || info.providers.length === 0 ? {} : { providers: info.providers }),
209
+ }, options.logger);
136
210
  })
137
211
  .catch((error) => {
138
212
  options.logger?.error?.("TUI info provider failed.", { error: errorToMessage(error) });
139
213
  sendJsonError(res, 500, error);
140
214
  });
141
215
  });
216
+ app.get(modelsPath, (req, res, next) => {
217
+ if (!authorize(req, res, apiKey))
218
+ return;
219
+ if (options.modelCatalog === undefined) {
220
+ sendJsonError(res, 404, new TuiAdapterError("invalid_request", "The model catalog is unavailable."));
221
+ return;
222
+ }
223
+ let request;
224
+ try {
225
+ request = normalizeModelCatalogRequest(req);
226
+ }
227
+ catch (error) {
228
+ next(error);
229
+ return;
230
+ }
231
+ let page;
232
+ try {
233
+ // The catalog is a trust boundary only in the sense that the provider is
234
+ // host-owned; a throwing supplier must still fail as a server error
235
+ // rather than silently serving an empty catalog (see the "totality"
236
+ // contract in the composition layer — that layer never throws).
237
+ page = options.modelCatalog(request);
238
+ }
239
+ catch (error) {
240
+ next(error);
241
+ return;
242
+ }
243
+ try {
244
+ sendBoundedModelCatalog(res, page);
245
+ }
246
+ catch (error) {
247
+ next(error);
248
+ }
249
+ });
142
250
  app.get(jobsPath, (req, res, next) => {
143
251
  if (!authorize(req, res, processJobsBearer))
144
252
  return;
@@ -202,12 +310,81 @@ export async function startTuiAdapter(options) {
202
310
  }
203
311
  });
204
312
  });
313
+ app.get(monitorsPath, (req, res, next) => {
314
+ if (!authorize(req, res, monitorsBearer))
315
+ return;
316
+ if (options.monitors === undefined || monitorsBearer === undefined) {
317
+ sendJsonError(res, 404, new TuiAdapterError("invalid_request", "Monitors are unavailable."));
318
+ return;
319
+ }
320
+ void options.monitors.list()
321
+ .then((monitors) => {
322
+ res.status(200).json({ monitors: parseMonitorProjections(monitors) });
323
+ })
324
+ .catch(next);
325
+ });
326
+ app.get(monitorPath, (req, res, next) => {
327
+ if (!authorize(req, res, monitorsBearer))
328
+ return;
329
+ if (options.monitors === undefined || monitorsBearer === undefined) {
330
+ sendJsonError(res, 404, new TuiAdapterError("invalid_request", "Monitors are unavailable."));
331
+ return;
332
+ }
333
+ const monitorId = boundedMonitorId(req.params.monitorId);
334
+ if (monitorId === undefined) {
335
+ sendJsonError(res, 400, new TuiAdapterError("invalid_request", "A bounded monitorId is required."));
336
+ return;
337
+ }
338
+ void options.monitors.get(monitorId)
339
+ .then((monitor) => {
340
+ if (monitor === undefined) {
341
+ res.status(404).json({ error: { code: "monitor_not_found", message: "Monitor was not found." } });
342
+ }
343
+ else {
344
+ res.status(200).json(parseMonitorProjection(monitor));
345
+ }
346
+ })
347
+ .catch(next);
348
+ });
349
+ app.post(monitorCancelPath, (req, res, next) => {
350
+ if (!authorize(req, res, monitorsBearer))
351
+ return;
352
+ if (options.monitors === undefined || monitorsBearer === undefined) {
353
+ sendJsonError(res, 404, new TuiAdapterError("invalid_request", "Monitors are unavailable."));
354
+ return;
355
+ }
356
+ const monitorId = boundedMonitorId(req.params.monitorId);
357
+ if (monitorId === undefined) {
358
+ sendJsonError(res, 400, new TuiAdapterError("invalid_request", "A bounded monitorId is required."));
359
+ return;
360
+ }
361
+ void options.monitors.cancel(monitorId)
362
+ .then((monitor) => {
363
+ res.status(200).json(parseMonitorProjection(monitor));
364
+ })
365
+ .catch((error) => {
366
+ const code = typeof error === "object" && error !== null
367
+ ? error.code
368
+ : undefined;
369
+ if (code === "monitor_not_found") {
370
+ res.status(404).json({ error: { code, message: errorToMessage(error) } });
371
+ }
372
+ else if (code === "monitor_conflict") {
373
+ res.status(409).json({ error: { code, message: errorToMessage(error) } });
374
+ }
375
+ else {
376
+ next(error);
377
+ }
378
+ });
379
+ });
205
380
  // Keep the enlarged parser scoped to turn submission. 64 MiB of decoded
206
381
  // files expands to about 85.4 MiB in base64, while info/cancel stay bodyless.
207
382
  app.post(turnsPath, express.json({ limit: MAX_TURN_BODY_BYTES }), (req, res) => {
208
383
  if (!authorize(req, res, apiKey)) {
209
384
  return;
210
385
  }
386
+ if (!authorizeMonitorWake(req, res, req.body?.processJobWakeDeliveryKey, monitorsBearer))
387
+ return;
211
388
  void handleTurn(req, res).catch((error) => {
212
389
  options.logger?.error?.("TUI turn failed before response.", { error: errorToMessage(error) });
213
390
  if (!res.headersSent) {
@@ -350,6 +527,36 @@ export async function startTuiAdapter(options) {
350
527
  res.status(200).json({ recorded: true, conversationId: body.conversationId });
351
528
  }).catch(next);
352
529
  });
530
+ app.post(contextImportPath, (_req, res, next) => {
531
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
532
+ next();
533
+ }, express.json({ limit: MAX_CONTEXT_IMPORT_BODY_BYTES, strict: true }), (req, res, next) => {
534
+ if (!authorize(req, res, apiKey))
535
+ return;
536
+ if (typeof options.responder.importContext !== "function") {
537
+ sendContextImportError(res, 501, "context_import_unsupported", "This responder does not support canonical context import.", "unsupported");
538
+ return;
539
+ }
540
+ let normalized;
541
+ try {
542
+ normalized = normalizeContextImportBody(req.params.conversationId, req.body);
543
+ }
544
+ catch (error) {
545
+ next(error);
546
+ return;
547
+ }
548
+ void options.responder.importContext(normalized.conversationId, normalized.request).then((result) => {
549
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
550
+ if (result.status === "conflict") {
551
+ sendContextImportError(res, 409, "context_import_conflict", "Canonical context import conflicts with existing history.", result.reason);
552
+ return;
553
+ }
554
+ res.status(200).json({ imported: true, status: result.status, conversationId: normalized.conversationId });
555
+ }).catch((error) => {
556
+ options.logger?.error?.("TUI context import failed.", { error: errorToMessage(error) });
557
+ sendContextImportError(res, 500, "context_import_failed", "Canonical context import failed.", "operation_failed");
558
+ });
559
+ });
353
560
  app.post(liveInputPath, express.json({ limit: MAX_LIVE_INPUT_BODY_BYTES, strict: true }), (req, res, next) => {
354
561
  if (!authorize(req, res, apiKey))
355
562
  return;
@@ -368,14 +575,86 @@ export async function startTuiAdapter(options) {
368
575
  || (body.deliveryKey !== undefined
369
576
  && (typeof body.deliveryKey !== "string"
370
577
  || body.deliveryKey.trim().length === 0
371
- || body.deliveryKey.length > 1_024))) {
578
+ || body.deliveryKey.length > 1_024))
579
+ || (body.targetTurnId !== undefined
580
+ && (typeof body.targetTurnId !== "string" || body.targetTurnId.trim().length === 0 || body.targetTurnId.length > 4_096))
581
+ || (body.targetRunId !== undefined
582
+ && (typeof body.targetRunId !== "string" || body.targetRunId.trim().length === 0 || body.targetRunId.length > 4_096))) {
372
583
  next(new TuiAdapterError("invalid_request", `Live input requires id, receivedAt, and 1-${String(AGENT_LIVE_INPUT_MAX_CHARACTERS)} text characters.`));
373
584
  return;
374
585
  }
586
+ if (!authorizeMonitorWake(req, res, body.deliveryKey, monitorsBearer))
587
+ return;
375
588
  if (typeof options.responder.offerLiveInput !== "function") {
376
589
  res.status(200).json({ status: "unavailable", reason: "unsupported" });
377
590
  return;
378
591
  }
592
+ const inputId = body.id;
593
+ const inputText = body.text;
594
+ const receivedAt = body.receivedAt;
595
+ const targetTurnId = typeof body.targetTurnId === "string" ? body.targetTurnId : undefined;
596
+ const explicitRunId = typeof body.targetRunId === "string" ? body.targetRunId : undefined;
597
+ if (targetTurnId !== undefined) {
598
+ const target = liveInputTargets.get(liveInputTargetKey(conversationId, targetTurnId));
599
+ if (target === undefined || target.state === "closed") {
600
+ res.status(200).json({ status: "unavailable", reason: "inactive" });
601
+ return;
602
+ }
603
+ const offerToTarget = (runId) => {
604
+ if (res.writableEnded || res.destroyed)
605
+ return;
606
+ if (runId === undefined || (explicitRunId !== undefined && explicitRunId !== runId)) {
607
+ res.status(200).json({ status: "unavailable", reason: "inactive" });
608
+ return;
609
+ }
610
+ try {
611
+ settleLiveInputOffer(res, options.responder.offerLiveInput({
612
+ conversationId,
613
+ id: inputId,
614
+ text: inputText,
615
+ receivedAt,
616
+ targetRunId: runId,
617
+ ...(typeof body.deliveryKey === "string" ? { deliveryKey: body.deliveryKey } : {}),
618
+ }));
619
+ }
620
+ catch (error) {
621
+ next(error);
622
+ }
623
+ };
624
+ if (target.state === "ready") {
625
+ offerToTarget(target.runId);
626
+ return;
627
+ }
628
+ if (target.waiters.size >= MAX_TARGET_WAITERS_PER_OPERATION || pendingTargetWaiters >= MAX_TARGET_WAITERS_GLOBAL) {
629
+ res.status(200).json({ status: "unavailable", reason: "full" });
630
+ return;
631
+ }
632
+ pendingTargetWaiters += 1;
633
+ let detached = false;
634
+ let timer;
635
+ const abort = () => { detach(); };
636
+ const settle = (runId) => {
637
+ detach();
638
+ offerToTarget(runId);
639
+ };
640
+ const detach = () => {
641
+ if (detached)
642
+ return;
643
+ detached = true;
644
+ if (timer !== undefined)
645
+ clearTimeout(timer);
646
+ req.off("aborted", abort);
647
+ res.off("close", abort);
648
+ target.waiters.delete(settle);
649
+ pendingTargetWaiters -= 1;
650
+ };
651
+ target.waiters.add(settle);
652
+ timer = setTimeout(() => { settle(undefined); }, TARGET_WAITER_TIMEOUT_MS);
653
+ timer.unref?.();
654
+ req.once("aborted", abort);
655
+ res.once("close", abort);
656
+ return;
657
+ }
379
658
  let offer;
380
659
  try {
381
660
  offer = options.responder.offerLiveInput({
@@ -383,6 +662,7 @@ export async function startTuiAdapter(options) {
383
662
  id: body.id,
384
663
  text: body.text,
385
664
  receivedAt: body.receivedAt,
665
+ ...(explicitRunId === undefined ? {} : { targetRunId: explicitRunId }),
386
666
  ...(typeof body.deliveryKey === "string" ? { deliveryKey: body.deliveryKey } : {}),
387
667
  });
388
668
  }
@@ -390,13 +670,7 @@ export async function startTuiAdapter(options) {
390
670
  next(error);
391
671
  return;
392
672
  }
393
- if (offer.status === "unavailable") {
394
- res.status(200).json(offer);
395
- return;
396
- }
397
- void offer.settled.then((settlement) => {
398
- res.status(200).json(settlement);
399
- }).catch(next);
673
+ settleLiveInputOffer(res, offer);
400
674
  });
401
675
  app.get(askPath, (req, res) => {
402
676
  if (!authorize(req, res, apiKey))
@@ -552,6 +826,158 @@ export async function startTuiAdapter(options) {
552
826
  next(error);
553
827
  }
554
828
  });
829
+ const requireProviderAuth = (req, res) => {
830
+ if (options.providerAuth === undefined) {
831
+ sendJsonError(res, 404, new TuiAdapterError("invalid_request", "Provider authentication is unavailable."));
832
+ return undefined;
833
+ }
834
+ return authorize(req, res, apiKey) ? options.providerAuth : undefined;
835
+ };
836
+ const sendProviderAuth = (res, status, body) => {
837
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
838
+ res.status(status).json(body);
839
+ };
840
+ app.use(providerAuthPath, (_req, res, next) => {
841
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
842
+ next();
843
+ });
844
+ app.get(providerAuthPath, (req, res, next) => {
845
+ const providerAuth = requireProviderAuth(req, res);
846
+ if (providerAuth === undefined)
847
+ return;
848
+ void providerAuth.status()
849
+ .then((snapshot) => sendProviderAuth(res, 200, parseProviderAuthStatusSnapshot(snapshot)))
850
+ .catch(next);
851
+ });
852
+ app.post(providerAuthSessionsPath, express.json({ limit: MAX_PROVIDER_AUTH_BODY_BYTES, strict: true }), (req, res, next) => {
853
+ const providerAuth = requireProviderAuth(req, res);
854
+ if (providerAuth === undefined)
855
+ return;
856
+ let input;
857
+ try {
858
+ input = parseProviderAuthSessionStartInput(req.body);
859
+ }
860
+ catch (error) {
861
+ next(error);
862
+ return;
863
+ }
864
+ void providerAuth.start(input)
865
+ .then((snapshot) => sendProviderAuth(res, 201, parseProviderAuthSessionSnapshot(snapshot)))
866
+ .catch(next);
867
+ });
868
+ app.get(providerAuthSessionPath, (req, res, next) => {
869
+ const providerAuth = requireProviderAuth(req, res);
870
+ if (providerAuth === undefined)
871
+ return;
872
+ const sessionId = boundedProviderAuthSessionId(req.params.sessionId);
873
+ if (sessionId === undefined) {
874
+ next(new TuiAdapterError("invalid_request", "A bounded provider auth session id is required."));
875
+ return;
876
+ }
877
+ void providerAuth.get(sessionId).then((snapshot) => {
878
+ if (snapshot === undefined) {
879
+ sendJsonError(res, 404, new ProviderAuthOperationError("provider_auth_not_found", "Provider authentication session was not found.", 404));
880
+ }
881
+ else {
882
+ sendProviderAuth(res, 200, parseProviderAuthSessionSnapshot(snapshot));
883
+ }
884
+ }).catch(next);
885
+ });
886
+ app.post(providerAuthInputPath, express.json({ limit: MAX_PROVIDER_AUTH_BODY_BYTES, strict: true }), (req, res, next) => {
887
+ const providerAuth = requireProviderAuth(req, res);
888
+ if (providerAuth === undefined)
889
+ return;
890
+ const sessionId = boundedProviderAuthSessionId(req.params.sessionId);
891
+ if (sessionId === undefined) {
892
+ next(new TuiAdapterError("invalid_request", "A bounded provider auth session id is required."));
893
+ return;
894
+ }
895
+ let input;
896
+ try {
897
+ input = parseProviderAuthSessionInput(req.body);
898
+ }
899
+ catch (error) {
900
+ next(error);
901
+ return;
902
+ }
903
+ void providerAuth.submit(sessionId, input)
904
+ .then((snapshot) => sendProviderAuth(res, 200, parseProviderAuthSessionSnapshot(snapshot)))
905
+ .catch(next);
906
+ });
907
+ app.delete(providerAuthSessionPath, (req, res, next) => {
908
+ const providerAuth = requireProviderAuth(req, res);
909
+ if (providerAuth === undefined)
910
+ return;
911
+ const sessionId = boundedProviderAuthSessionId(req.params.sessionId);
912
+ if (sessionId === undefined) {
913
+ next(new TuiAdapterError("invalid_request", "A bounded provider auth session id is required."));
914
+ return;
915
+ }
916
+ void providerAuth.cancel(sessionId).then(() => {
917
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
918
+ res.status(204).end();
919
+ }).catch(next);
920
+ });
921
+ app.post(providerAuthChecksPath, express.json({ limit: MAX_PROVIDER_AUTH_BODY_BYTES, strict: true }), (req, res, next) => {
922
+ const providerAuth = requireProviderAuth(req, res);
923
+ if (providerAuth === undefined)
924
+ return;
925
+ if (providerAuth.checks === undefined) {
926
+ next(new ProviderAuthOperationError("provider_auth_unavailable", "Provider checks are unavailable.", 503));
927
+ return;
928
+ }
929
+ let input;
930
+ try {
931
+ input = parseProviderAuthCheckStartInput(req.body);
932
+ }
933
+ catch (error) {
934
+ next(error);
935
+ return;
936
+ }
937
+ void providerAuth.checks.start(input)
938
+ .then((snapshot) => sendProviderAuth(res, 201, parseProviderAuthCheckSessionSnapshot(snapshot)))
939
+ .catch(next);
940
+ });
941
+ app.get(providerAuthCheckPath, (req, res, next) => {
942
+ const providerAuth = requireProviderAuth(req, res);
943
+ if (providerAuth === undefined)
944
+ return;
945
+ if (providerAuth.checks === undefined) {
946
+ next(new ProviderAuthOperationError("provider_auth_unavailable", "Provider checks are unavailable.", 503));
947
+ return;
948
+ }
949
+ const checkId = boundedProviderAuthSessionId(req.params.checkId);
950
+ if (checkId === undefined) {
951
+ next(new TuiAdapterError("invalid_request", "A bounded provider auth check id is required."));
952
+ return;
953
+ }
954
+ void providerAuth.checks.get(checkId).then((snapshot) => {
955
+ if (snapshot === undefined) {
956
+ sendJsonError(res, 404, new ProviderAuthOperationError("provider_auth_not_found", "Provider auth check was not found.", 404));
957
+ }
958
+ else {
959
+ sendProviderAuth(res, 200, parseProviderAuthCheckSessionSnapshot(snapshot));
960
+ }
961
+ }).catch(next);
962
+ });
963
+ app.delete(providerAuthCheckPath, (req, res, next) => {
964
+ const providerAuth = requireProviderAuth(req, res);
965
+ if (providerAuth === undefined)
966
+ return;
967
+ if (providerAuth.checks === undefined) {
968
+ next(new ProviderAuthOperationError("provider_auth_unavailable", "Provider checks are unavailable.", 503));
969
+ return;
970
+ }
971
+ const checkId = boundedProviderAuthSessionId(req.params.checkId);
972
+ if (checkId === undefined) {
973
+ next(new TuiAdapterError("invalid_request", "A bounded provider auth check id is required."));
974
+ return;
975
+ }
976
+ void providerAuth.checks.cancel(checkId).then(() => {
977
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
978
+ res.status(204).end();
979
+ }).catch(next);
980
+ });
555
981
  app.use((error, _req, res, next) => {
556
982
  if (res.headersSent) {
557
983
  next(error);
@@ -569,6 +995,13 @@ export async function startTuiAdapter(options) {
569
995
  sendJsonError(res, error.status, error);
570
996
  return;
571
997
  }
998
+ if (error instanceof ProviderAuthOperationError) {
999
+ if (error.retryAfterSeconds !== undefined) {
1000
+ res.setHeader("Retry-After", String(error.retryAfterSeconds));
1001
+ }
1002
+ sendJsonError(res, error.status, error);
1003
+ return;
1004
+ }
572
1005
  const isClientError = codeOf(error) === "invalid_request" ||
573
1006
  (error instanceof SyntaxError && error.status === 400);
574
1007
  sendJsonError(res, isClientError ? 400 : 500, error);
@@ -600,16 +1033,43 @@ export async function startTuiAdapter(options) {
600
1033
  const url = `http://${hostForUrl(host)}:${boundPort}`;
601
1034
  async function handleTurn(req, res) {
602
1035
  const body = normalizeTurnBody(req.body, options.requestToolEnvironment);
1036
+ const requestId = randomUUID();
1037
+ const web = isRecord(body.metadata.web) ? body.metadata.web : undefined;
1038
+ const webTurnId = body.client === "web" && typeof web?.turnId === "string" && web.turnId.length > 0
1039
+ ? web.turnId
1040
+ : undefined;
1041
+ const targetKey = webTurnId === undefined || options.responder.liveInputOwnership?.version !== 1
1042
+ ? undefined
1043
+ : liveInputTargetKey(body.conversationId, webTurnId);
1044
+ if (targetKey !== undefined && liveInputTargets.has(targetKey)) {
1045
+ throw new TuiAdapterError("invalid_request", "Web turn is already active.");
1046
+ }
603
1047
  const controller = new AbortController();
604
1048
  activeTurns.add(controller);
605
1049
  if (stopping)
606
1050
  controller.abort(new Error("TUI adapter is stopping."));
607
- const requestId = randomUUID();
1051
+ let target;
1052
+ if (targetKey !== undefined) {
1053
+ target = { state: "pending", waiters: new Set() };
1054
+ liveInputTargets.set(targetKey, target);
1055
+ }
608
1056
  const request = {
609
1057
  conversationId: body.conversationId,
610
1058
  text: body.text,
611
1059
  abortSignal: controller.signal,
612
1060
  metadata: requestMetadata(body, requestId),
1061
+ ...(target === undefined ? {} : {
1062
+ onLiveInputOwnership: (event) => {
1063
+ if (target?.state === "closed")
1064
+ return;
1065
+ if (event.status === "ready") {
1066
+ settleTarget(target, event.runId);
1067
+ }
1068
+ else {
1069
+ settleTarget(target, undefined);
1070
+ }
1071
+ },
1072
+ }),
613
1073
  ...(body.attachments === undefined || body.attachments.length === 0
614
1074
  ? {}
615
1075
  : { attachments: body.attachments }),
@@ -646,6 +1106,11 @@ export async function startTuiAdapter(options) {
646
1106
  }).catch(() => undefined);
647
1107
  }
648
1108
  finally {
1109
+ if (target !== undefined) {
1110
+ settleTarget(target, undefined);
1111
+ if (targetKey !== undefined && liveInputTargets.get(targetKey) === target)
1112
+ liveInputTargets.delete(targetKey);
1113
+ }
649
1114
  activeTurns.delete(controller);
650
1115
  res.end();
651
1116
  }
@@ -660,9 +1125,13 @@ export async function startTuiAdapter(options) {
660
1125
  stop() {
661
1126
  stopPromise ??= (async () => {
662
1127
  stopping = true;
1128
+ for (const target of liveInputTargets.values()) {
1129
+ settleTarget(target, undefined);
1130
+ }
1131
+ liveInputTargets.clear();
663
1132
  for (const controller of activeTurns)
664
1133
  controller.abort(new Error("TUI adapter stopped."));
665
- await closeServerBounded(server);
1134
+ await Promise.all([closeServerBounded(server), options.providerAuth?.stop()]);
666
1135
  activeTurns.clear();
667
1136
  })();
668
1137
  return stopPromise;
@@ -1069,6 +1538,36 @@ function sendMcpAppError(res, error) {
1069
1538
  setPrivateMcpAppHeaders(res);
1070
1539
  sendJsonError(res, status, error);
1071
1540
  }
1541
+ function normalizeContextImportBody(rawConversationId, body) {
1542
+ const conversationId = normalizeOptionalString(typeof rawConversationId === "string" ? rawConversationId : undefined);
1543
+ if (conversationId === undefined
1544
+ || Buffer.byteLength(conversationId, "utf8") > AGENT_CONTEXT_IMPORT_MAX_CONVERSATION_ID_BYTES
1545
+ || conversationId.includes("\0")) {
1546
+ throw new TuiAdapterError("invalid_request", "A bounded conversationId is required.");
1547
+ }
1548
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
1549
+ throw new TuiAdapterError("invalid_request", "Request body must be a JSON object.");
1550
+ }
1551
+ const record = body;
1552
+ if (Object.keys(record).sort().join("\0") !== ["idempotencyKey", "text"].join("\0")) {
1553
+ throw new TuiAdapterError("invalid_request", "Request body must contain only text and idempotencyKey.");
1554
+ }
1555
+ if (typeof record.text !== "string"
1556
+ || record.text.trim().length === 0
1557
+ || Buffer.byteLength(record.text, "utf8") > AGENT_CONTEXT_IMPORT_MAX_TEXT_BYTES) {
1558
+ throw new TuiAdapterError("invalid_request", "text must be a string within the context import byte limit.");
1559
+ }
1560
+ if (typeof record.idempotencyKey !== "string"
1561
+ || record.idempotencyKey.trim().length === 0
1562
+ || record.idempotencyKey.includes("\0")
1563
+ || Buffer.byteLength(record.idempotencyKey, "utf8") > AGENT_CONTEXT_IMPORT_MAX_IDEMPOTENCY_KEY_BYTES) {
1564
+ throw new TuiAdapterError("invalid_request", "idempotencyKey must be a bounded non-empty UTF-8 string.");
1565
+ }
1566
+ return {
1567
+ conversationId,
1568
+ request: { text: record.text, idempotencyKey: record.idempotencyKey },
1569
+ };
1570
+ }
1072
1571
  function normalizeVerbatimBody(rawConversationId, body) {
1073
1572
  const conversationId = normalizeOptionalString(typeof rawConversationId === "string" ? rawConversationId : undefined);
1074
1573
  if (conversationId === undefined) {
@@ -1371,6 +1870,171 @@ async function resolveInfo(info) {
1371
1870
  }
1372
1871
  return info;
1373
1872
  }
1873
+ function normalizeModelCatalogRequest(req) {
1874
+ const rawProvider = typeof req.query.provider === "string" ? req.query.provider : undefined;
1875
+ const provider = normalizeOptionalString(rawProvider);
1876
+ if (provider !== undefined && Buffer.byteLength(provider, "utf8") > MAX_MODEL_CATALOG_PROVIDER_BYTES) {
1877
+ throw new TuiAdapterError("invalid_request", "provider is too large.");
1878
+ }
1879
+ const rawQuery = typeof req.query.q === "string" ? req.query.q : undefined;
1880
+ const query = normalizeOptionalString(rawQuery);
1881
+ if (query !== undefined && Buffer.byteLength(query, "utf8") > MAX_MODEL_CATALOG_QUERY_BYTES) {
1882
+ throw new TuiAdapterError("invalid_request", "q is too large.");
1883
+ }
1884
+ const rawCursor = typeof req.query.cursor === "string" ? req.query.cursor : undefined;
1885
+ const cursor = normalizeOptionalString(rawCursor);
1886
+ if (cursor !== undefined && Buffer.byteLength(cursor, "utf8") > MAX_MODEL_CATALOG_CURSOR_BYTES) {
1887
+ throw new TuiAdapterError("invalid_request", "cursor is too large.");
1888
+ }
1889
+ if (provider === undefined && query === undefined) {
1890
+ throw new TuiAdapterError("invalid_request", "provider or q is required.");
1891
+ }
1892
+ // `TuiModelCatalogRequest` documents the two modes as mutually exclusive and
1893
+ // suppliers honour that by servicing `provider` and ignoring `query`. Sending
1894
+ // both must therefore be a client error, not a silently provider-scoped page
1895
+ // that looks like it answered the search.
1896
+ if (provider !== undefined && query !== undefined) {
1897
+ throw new TuiAdapterError("invalid_request", "provider and q are mutually exclusive.");
1898
+ }
1899
+ const rawLimit = typeof req.query.limit === "string"
1900
+ ? Number(req.query.limit)
1901
+ : DEFAULT_MODEL_CATALOG_PAGE_SIZE;
1902
+ if (!Number.isSafeInteger(rawLimit) || rawLimit < 1 || rawLimit > MAX_MODEL_CATALOG_PAGE_SIZE) {
1903
+ throw new TuiAdapterError("invalid_request", `limit must be 1-${String(MAX_MODEL_CATALOG_PAGE_SIZE)}.`);
1904
+ }
1905
+ return {
1906
+ ...(provider === undefined ? {} : { provider }),
1907
+ ...(query === undefined ? {} : { query }),
1908
+ ...(cursor === undefined ? {} : { cursor }),
1909
+ limit: rawLimit,
1910
+ };
1911
+ }
1912
+ /**
1913
+ * `/v1/info` fields the fence may shed, least load-bearing first. Every one of
1914
+ * them is already optional on the wire — the response literal omits each when
1915
+ * its source is absent — so shedding produces a body that is still valid at
1916
+ * schema 1, which matters because `TUI_WIRE_SCHEMA` is compared with `!==` and
1917
+ * cannot be bumped. `schema`, `pid` and `capabilities` are never sheddable:
1918
+ * they are the liveness and negotiation half of the response.
1919
+ */
1920
+ const INFO_SHED_ORDER = [
1921
+ "modelOptions",
1922
+ "models",
1923
+ "providers",
1924
+ "skills",
1925
+ "label",
1926
+ "effort",
1927
+ "model",
1928
+ ];
1929
+ /**
1930
+ * Send `/v1/info` under the byte contract its consumer enforces.
1931
+ *
1932
+ * Every contributor to this body carries its own producer-side budget (see the
1933
+ * budget table in `agent-app`'s `channel-drivers/tui.ts`, and
1934
+ * `MAX_SKILL_REGISTRY_BYTES` for skills) and those budgets sum to 960 KiB. This
1935
+ * is the last-resort fence behind them: without it the ONLY enforcement of the
1936
+ * 1 MiB cap lived in the consumer, so any producer-side miss took the agent
1937
+ * offline instead of degrading it.
1938
+ *
1939
+ * It measures the exact string it sends rather than estimating, and sends that
1940
+ * string rather than re-serializing, so what was measured is what goes on the
1941
+ * wire. Shedding is logged at error level: a body that reaches this fence is a
1942
+ * producer bug, and it must not disappear silently.
1943
+ */
1944
+ function sendBoundedInfo(res, body, logger) {
1945
+ const candidate = { ...body };
1946
+ const dropped = [];
1947
+ let serialized = serializeInfoBody(candidate);
1948
+ while (serialized === undefined) {
1949
+ const field = largestSheddableInfoField(candidate);
1950
+ // Each pass removes one field from a finite set, so this terminates.
1951
+ if (field === undefined)
1952
+ break;
1953
+ delete candidate[field];
1954
+ dropped.push(field);
1955
+ serialized = serializeInfoBody(candidate);
1956
+ }
1957
+ if (serialized !== undefined) {
1958
+ if (dropped.length > 0) {
1959
+ logger?.error?.("TUI info body exceeded its wire budget; fields were dropped.", {
1960
+ droppedFields: dropped.join(","),
1961
+ });
1962
+ }
1963
+ res.status(200).type("application/json").send(serialized);
1964
+ return;
1965
+ }
1966
+ // Nothing sheddable is left and the remainder still will not fit (or will not
1967
+ // serialize at all). Fall back to the fixed liveness floor: a schema-1 body
1968
+ // small by construction, which keeps the agent reachable and its capability
1969
+ // negotiation honest rather than answering 500 and reading as offline.
1970
+ logger?.error?.("TUI info body could not be bounded; served the minimal liveness body.", {
1971
+ droppedFields: dropped.join(","),
1972
+ });
1973
+ res.status(200).type("application/json").send(JSON.stringify({
1974
+ schema: TUI_WIRE_SCHEMA,
1975
+ pid: process.pid,
1976
+ capabilities: { attachments: true },
1977
+ }));
1978
+ }
1979
+ /**
1980
+ * The optional field costing the most bytes right now.
1981
+ *
1982
+ * Shedding the biggest field first means the fence removes what is ACTUALLY
1983
+ * oversized instead of four innocent projections queued ahead of it: a 1.5 MiB
1984
+ * `skills` registry costs the console its skills, not its model picker as well.
1985
+ * Ties break towards the front of `INFO_SHED_ORDER` (least load-bearing first),
1986
+ * so the choice is deterministic. A field whose own value will not serialize
1987
+ * sorts first of all — it is the reason the body cannot be measured at all.
1988
+ */
1989
+ function largestSheddableInfoField(body) {
1990
+ let largest;
1991
+ let largestBytes = -1;
1992
+ for (const field of INFO_SHED_ORDER) {
1993
+ if (!(field in body))
1994
+ continue;
1995
+ const bytes = infoFieldBytes(body[field]);
1996
+ if (bytes > largestBytes) {
1997
+ largest = field;
1998
+ largestBytes = bytes;
1999
+ }
2000
+ }
2001
+ return largest;
2002
+ }
2003
+ /** Serialized size of one field value; `Infinity` when it cannot be serialized at all. */
2004
+ function infoFieldBytes(value) {
2005
+ try {
2006
+ const serialized = JSON.stringify(value);
2007
+ return typeof serialized === "string"
2008
+ ? Buffer.byteLength(serialized, "utf8")
2009
+ : Number.POSITIVE_INFINITY;
2010
+ }
2011
+ catch {
2012
+ return Number.POSITIVE_INFINITY;
2013
+ }
2014
+ }
2015
+ /** Serialize the candidate body, or `undefined` when it does not fit or does not serialize. */
2016
+ function serializeInfoBody(body) {
2017
+ let serialized;
2018
+ try {
2019
+ serialized = JSON.stringify(body);
2020
+ }
2021
+ catch {
2022
+ return undefined;
2023
+ }
2024
+ // `JSON.stringify` yields undefined for a non-serializable top-level value.
2025
+ if (typeof serialized !== "string")
2026
+ return undefined;
2027
+ return Buffer.byteLength(serialized, "utf8") > MAX_INFO_BODY_BYTES ? undefined : serialized;
2028
+ }
2029
+ function sendBoundedModelCatalog(res, page) {
2030
+ const body = JSON.stringify(page);
2031
+ if (Buffer.byteLength(body, "utf8") > MAX_MODEL_CATALOG_RESPONSE_BYTES) {
2032
+ // The supplier is expected to pre-bound pages; hitting this fence is a
2033
+ // producer bug, not a client mistake, so it reads as a 500.
2034
+ throw new TuiAdapterError("model_catalog_too_large", "Model catalog page exceeded its bounded wire contract.");
2035
+ }
2036
+ res.status(200).type("application/json").send(body);
2037
+ }
1374
2038
  function cronJobId(value) {
1375
2039
  const raw = typeof value === "string" ? value : undefined;
1376
2040
  const jobId = normalizeOptionalString(raw);
@@ -1421,6 +2085,16 @@ function sendBoundedCronJson(res, status, value) {
1421
2085
  }
1422
2086
  res.status(status).type("application/json").send(serialized);
1423
2087
  }
2088
+ /** A wake key identifies a flight; only the independent owner bearer authorizes it. */
2089
+ function authorizeMonitorWake(req, res, key, ownerBearer) {
2090
+ if (typeof key !== "string" || !key.trim().startsWith("monitor:"))
2091
+ return true;
2092
+ const presented = readAuthorizationBearer(req.header("x-mono-agent-monitor-wake-authorization"));
2093
+ if (ownerBearer !== undefined && presented !== undefined && bearerTokensEqual(presented, ownerBearer))
2094
+ return true;
2095
+ res.status(401).json({ error: { message: "Monitor wake requires owner authorization.", code: "invalid_api_key" } });
2096
+ return false;
2097
+ }
1424
2098
  function authorize(req, res, apiKey) {
1425
2099
  if (apiKey === undefined) {
1426
2100
  return true;
@@ -1432,13 +2106,81 @@ function authorize(req, res, apiKey) {
1432
2106
  res.status(401).json({ error: { message: "Invalid API key.", code: "invalid_api_key" } });
1433
2107
  return false;
1434
2108
  }
2109
+ function boundedProviderAuthSessionId(value) {
2110
+ return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= 128
2111
+ ? value
2112
+ : undefined;
2113
+ }
1435
2114
  function sendJsonError(res, status, error) {
1436
- res.status(status).json({
1437
- error: {
1438
- message: errorToMessage(error),
1439
- ...(codeOf(error) === undefined ? {} : { code: codeOf(error) }),
1440
- },
1441
- });
2115
+ res.status(status).type("application/json").send(boundedErrorBody(error));
2116
+ }
2117
+ function sendContextImportError(res, status, code, message, reason) {
2118
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
2119
+ res.status(status).json({ error: { code, message, reason } });
2120
+ }
2121
+ /** Appended to a message the fence had to cut, so a reader is never handed a
2122
+ * truncated diagnostic as if it were the whole one. */
2123
+ const TRUNCATED_MESSAGE_SUFFIX = "\u2026 [truncated]";
2124
+ /**
2125
+ * Serialize one error envelope under the SHARED `/v1/info` body cap.
2126
+ *
2127
+ * `sendBoundedInfo` bounds a success body by shedding fields; an error body has
2128
+ * no field to shed — it is one diagnostic message — so it is bounded by
2129
+ * clamping that message instead, which is the one place truncating is
2130
+ * obviously correct. Bounding success and not failure is not a wire contract:
2131
+ * before this, a rejecting `info` provider answered whatever its message
2132
+ * measured (a real probe: 1,052,696 bytes against a 1,048,576-byte cap),
2133
+ * because the route's `.catch` lands here and this responder had no bound.
2134
+ *
2135
+ * `code` survives clamping — the console and the TUI switch on it — and the
2136
+ * message keeps its head, so a clamped body still names what failed.
2137
+ */
2138
+ function boundedErrorBody(error) {
2139
+ const message = errorToMessage(error);
2140
+ const rawCode = codeOf(error);
2141
+ // A `code` is an identifier, kept whole whenever it can be. A "code" that
2142
+ // alone overruns the cap is not an identifier, and dropping it is better than
2143
+ // letting it crowd out the message that explains the failure.
2144
+ const code = rawCode !== undefined
2145
+ && Buffer.byteLength(errorEnvelope(TRUNCATED_MESSAGE_SUFFIX, rawCode), "utf8") <= MAX_INFO_BODY_BYTES
2146
+ ? rawCode
2147
+ : undefined;
2148
+ const full = errorEnvelope(message, code);
2149
+ if (Buffer.byteLength(full, "utf8") <= MAX_INFO_BODY_BYTES)
2150
+ return full;
2151
+ return errorEnvelope(clampErrorMessage(message, code), code);
2152
+ }
2153
+ function errorEnvelope(message, code) {
2154
+ return JSON.stringify({ error: { message, ...(code === undefined ? {} : { code }) } });
2155
+ }
2156
+ /**
2157
+ * A prefix of `message` whose envelope fits, plus the truncation marker.
2158
+ *
2159
+ * Measured against the SERIALIZED envelope, not the raw string, and shrunk in
2160
+ * PROPORTION to how far over the envelope is. Both halves matter: the overshoot
2161
+ * is counted in bytes while `kept` is counted in code units, and one unit can
2162
+ * escape to six bytes (`\u0007` -> `\\u0007`), so subtracting a byte overshoot
2163
+ * from a unit count deletes the entire message the moment escaping is heavy —
2164
+ * leaving a body that is bounded but says nothing about what failed.
2165
+ */
2166
+ function clampErrorMessage(message, code) {
2167
+ let kept = message;
2168
+ for (;;) {
2169
+ const candidate = kept + TRUNCATED_MESSAGE_SUFFIX;
2170
+ const bytes = Buffer.byteLength(errorEnvelope(candidate, code), "utf8");
2171
+ // The marker-only envelope was proven to fit before this was called, so the
2172
+ // loop cannot spin at length 0.
2173
+ if (bytes <= MAX_INFO_BODY_BYTES || kept.length === 0)
2174
+ return candidate;
2175
+ // Never drop less than an eighth, so a proportion that barely moves (the
2176
+ // envelope's own fixed overhead) still converges in O(log n) passes.
2177
+ const proportional = Math.floor(kept.length * (MAX_INFO_BODY_BYTES / bytes));
2178
+ kept = kept.slice(0, Math.min(proportional, kept.length - Math.ceil(kept.length / 8)));
2179
+ // Never end on a lone high surrogate split out of a pair.
2180
+ const last = kept.charCodeAt(kept.length - 1);
2181
+ if (last >= 0xd800 && last <= 0xdbff)
2182
+ kept = kept.slice(0, -1);
2183
+ }
1442
2184
  }
1443
2185
  function sendBoundedJobs(res, jobs) {
1444
2186
  const body = serializeBoundedJobs(jobs);
@@ -1514,4 +2256,8 @@ function normalizeBasePath(basePath) {
1514
2256
  }
1515
2257
  return basePath.length === 1 ? "" : basePath.replace(/\/+$/u, "");
1516
2258
  }
2259
+ function boundedMonitorId(value) {
2260
+ const monitorId = normalizeOptionalString(typeof value === "string" ? value : undefined);
2261
+ return monitorId === undefined || monitorId.length > 256 ? undefined : monitorId;
2262
+ }
1517
2263
  //# sourceMappingURL=server.js.map