@zackbart/connecta 0.24.2 → 0.24.4

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.
Files changed (97) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +5 -6
  4. package/dist/auth/bearer.js +2 -0
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/branding.d.ts +31 -2
  8. package/dist/branding.js +116 -8
  9. package/dist/call-admission.d.ts +4 -0
  10. package/dist/call-admission.js +26 -0
  11. package/dist/catalog-drift.js +9 -4
  12. package/dist/catalog-service.d.ts +2 -0
  13. package/dist/catalog-service.js +25 -8
  14. package/dist/catalog.d.ts +2 -0
  15. package/dist/catalog.js +246 -121
  16. package/dist/connectors/api.d.ts +1 -1
  17. package/dist/connectors/api.js +21 -3
  18. package/dist/connectors/guarded-fetch.d.ts +6 -2
  19. package/dist/connectors/guarded-fetch.js +61 -24
  20. package/dist/connectors/remote-mcp.js +92 -57
  21. package/dist/errors.d.ts +28 -3
  22. package/dist/errors.js +60 -1
  23. package/dist/execute.js +85 -23
  24. package/dist/executor-result.js +3 -1
  25. package/dist/executors/quickjs-child.js +5 -1
  26. package/dist/executors/quickjs-protocol.d.ts +4 -0
  27. package/dist/executors/quickjs-runtime.d.ts +1 -1
  28. package/dist/executors/quickjs-runtime.js +38 -21
  29. package/dist/executors/quickjs.js +68 -27
  30. package/dist/index.d.ts +15 -1
  31. package/dist/index.js +36 -4
  32. package/dist/invocation.js +134 -93
  33. package/dist/mcp-result.js +3 -2
  34. package/dist/meta-tools.js +191 -36
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +38 -1
  37. package/dist/operator-ui/view.js +71 -0
  38. package/dist/providers/cloudflare.d.ts +14 -2
  39. package/dist/providers/cloudflare.js +107 -16
  40. package/dist/providers/linear.d.ts +26 -4
  41. package/dist/providers/linear.js +19 -4
  42. package/dist/providers/mixpanel.d.ts +16 -3
  43. package/dist/providers/mixpanel.js +13 -2
  44. package/dist/providers/notion.d.ts +8 -1
  45. package/dist/providers/notion.js +83 -10
  46. package/dist/providers/revenuecat.d.ts +30 -4
  47. package/dist/providers/revenuecat.js +42 -4
  48. package/dist/providers/stripe.d.ts +7 -1
  49. package/dist/providers/stripe.js +30 -4
  50. package/dist/providers/vercel.js +11 -1
  51. package/dist/registry.d.ts +22 -2
  52. package/dist/registry.js +101 -13
  53. package/dist/routes/mcp.d.ts +4 -1
  54. package/dist/routes/mcp.js +84 -13
  55. package/dist/routes/oauth.js +4 -0
  56. package/dist/routes/shared.d.ts +1 -0
  57. package/dist/routes/shared.js +4 -4
  58. package/dist/server.js +15 -3
  59. package/dist/skills.js +6 -5
  60. package/dist/storage/file.d.ts +6 -2
  61. package/dist/storage/file.js +312 -34
  62. package/dist/storage/memory.js +12 -1
  63. package/dist/types.d.ts +37 -0
  64. package/dist/ui.js +18 -10
  65. package/dist/validate.js +3 -3
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/documentation/architecture.md +193 -165
  69. package/documentation/auth.md +199 -145
  70. package/documentation/code-mode.md +433 -316
  71. package/documentation/meta-tools.md +363 -355
  72. package/examples/worker/AGENTS.md +2 -1
  73. package/examples/worker/README.md +12 -10
  74. package/examples/worker/src/index.ts +12 -15
  75. package/package.json +1 -2
  76. package/templates/node/.env.example +3 -3
  77. package/templates/node/AGENTS.md +5 -4
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +23 -22
  81. package/documentation/call-admission.md +0 -142
  82. package/documentation/cloudflare.md +0 -471
  83. package/documentation/connector-guides.md +0 -176
  84. package/documentation/connectors.md +0 -389
  85. package/documentation/linear.md +0 -193
  86. package/documentation/mixpanel.md +0 -160
  87. package/documentation/notion.md +0 -308
  88. package/documentation/operations.md +0 -359
  89. package/documentation/operator-ui.md +0 -135
  90. package/documentation/optional-modules-upgrade.md +0 -243
  91. package/documentation/provider-conventions.md +0 -722
  92. package/documentation/request-admission.md +0 -170
  93. package/documentation/revenuecat.md +0 -305
  94. package/documentation/storage-and-credentials.md +0 -201
  95. package/documentation/stripe.md +0 -262
  96. package/documentation/upgrading.md +0 -754
  97. package/documentation/vercel.md +0 -241
package/dist/execute.js CHANGED
@@ -130,7 +130,8 @@ export class EmitCollector {
130
130
  accept(raw) {
131
131
  const block = requireEmittedBlock(raw);
132
132
  if (this.blocks.length >= this.maxBlocks) {
133
- throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 remaining`);
133
+ // M5: distinguish exhausted block slots from the remaining byte budget.
134
+ throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - this.bytes} of ${this.maxBytes} serialized bytes remaining`);
134
135
  }
135
136
  const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
136
137
  if (this.bytes + size > this.maxBytes) {
@@ -171,6 +172,48 @@ function guestFailureSecret() {
171
172
  crypto.getRandomValues(words);
172
173
  return Array.from(words, (word) => word.toString(16).padStart(8, "0")).join("");
173
174
  }
175
+ /**
176
+ * E1/X11: bound the serialized frame, including JSON escapes, before transport.
177
+ * Keep optional recovery whole: clipping an address or echoed args could turn
178
+ * recovery into a different call. Oversized metadata is omitted instead.
179
+ */
180
+ function boundedGuestFailure(failure) {
181
+ const boundText = (text, maxChars) => {
182
+ if (JSON.stringify(text).length <= maxChars)
183
+ return text;
184
+ let low = 0;
185
+ let high = Math.min(text.length, maxChars);
186
+ while (low < high) {
187
+ const mid = Math.ceil((low + high) / 2);
188
+ if (JSON.stringify(`${text.slice(0, mid)}…`).length <= maxChars)
189
+ low = mid;
190
+ else
191
+ high = mid - 1;
192
+ }
193
+ // Do not manufacture a lone surrogate when clipping a Unicode message.
194
+ if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1]))
195
+ low--;
196
+ return `${text.slice(0, low)}…`;
197
+ };
198
+ let details = {
199
+ ...failure.details,
200
+ code: boundText(failure.details.code, 128),
201
+ message: boundText(failure.details.message, 2_000),
202
+ };
203
+ // 3,700 plus the prefix and 32-character secret stays below QuickJS's
204
+ // 4,000-character error bound, with room for bridge-added context.
205
+ if (JSON.stringify(details).length > 3_700) {
206
+ details = {
207
+ code: details.code,
208
+ message: details.message,
209
+ retryable: details.retryable,
210
+ ...(details.retryAfterMs !== undefined
211
+ ? { retryAfterMs: details.retryAfterMs }
212
+ : {}),
213
+ };
214
+ }
215
+ return new InvocationFailure(details);
216
+ }
174
217
  function framedGuestFailure(secret, failure) {
175
218
  const framed = new InvocationFailure(failure.details);
176
219
  framed.message =
@@ -191,7 +234,8 @@ function guestErrorPrelude(failureSecret) {
191
234
  function ConnectaError(message, options) {
192
235
  let details;
193
236
  if (typeof message === "string" && startsWith(message, failurePrefix)) {
194
- try { details = parse(slice(message, failurePrefix.length)); } catch {}
237
+ try { details = parse(slice(message, failurePrefix.length)); }
238
+ catch { message = "Invalid host failure frame."; }
195
239
  }
196
240
  const error = construct(
197
241
  NativeError,
@@ -238,17 +282,18 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
238
282
  const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
239
283
  const failureSecret = guestFailureSecret();
240
284
  let hostCalls = 0;
285
+ // L4/M7: discovery and invocation spend the same budget; emit does not.
286
+ const spendHostCall = () => {
287
+ hostCalls++;
288
+ if (hostCalls > maxHostCalls) {
289
+ throw guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`);
290
+ }
291
+ };
241
292
  const invocationContext = () => ({
242
293
  source: "execute_code",
243
294
  timeoutMs: hostCallTimeoutMs,
244
295
  ...(limits.signal !== undefined ? { requestSignal: limits.signal } : {}),
245
296
  unwrapResult: true,
246
- beforeDispatch: () => {
247
- hostCalls++;
248
- if (hostCalls > maxHostCalls) {
249
- throw guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`);
250
- }
251
- },
252
297
  });
253
298
  /**
254
299
  * Discovery policy failures use the same thrown vocabulary as calls and
@@ -268,6 +313,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
268
313
  const timedCatalog = async (operation, fn) => {
269
314
  const started = Date.now();
270
315
  try {
316
+ spendHostCall();
271
317
  const result = await fn();
272
318
  limits.diagnostics?.recordCatalog(operation, Date.now() - started, true, result);
273
319
  return result;
@@ -278,6 +324,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
278
324
  }
279
325
  };
280
326
  const callAddress = async (address, args) => {
327
+ spendHostCall();
281
328
  const outcome = await invocation.invoke(String(address), args ?? {}, invocationContext());
282
329
  limits.diagnostics?.recordCall(outcome);
283
330
  if (!outcome.ok)
@@ -296,7 +343,9 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
296
343
  }
297
344
  limits.emitCollector.accept(block);
298
345
  },
299
- search: async (raw) => timedCatalog("search", () => typedDiscovery(async () => {
346
+ // Not `async`: a synchronous budget refusal must reject the same promise
347
+ // the transport wrapper awaits, or workerd reports an unhandled rejection.
348
+ search: (raw) => timedCatalog("search", () => typedDiscovery(async () => {
300
349
  const args = (raw ?? {});
301
350
  const result = flatSearchResult(await catalog.search({
302
351
  ...args,
@@ -305,7 +354,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
305
354
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
306
355
  return result;
307
356
  })),
308
- describe: async (raw) => timedCatalog("describe", () => typedDiscovery(async () => {
357
+ describe: (raw) => timedCatalog("describe", () => typedDiscovery(async () => {
309
358
  const args = (raw ?? {});
310
359
  const result = { tools: await catalog.describe(args) };
311
360
  boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
@@ -320,8 +369,9 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
320
369
  }
321
370
  catch (err) {
322
371
  if (err instanceof InvocationFailure) {
323
- const framed = framedGuestFailure(failureSecret, err);
324
- limits.onInvocationFailure?.(err);
372
+ const failure = boundedGuestFailure(err);
373
+ const framed = framedGuestFailure(failureSecret, failure);
374
+ limits.onInvocationFailure?.(failure);
325
375
  throw framed;
326
376
  }
327
377
  throw err;
@@ -377,6 +427,8 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
377
427
  signal: controller.signal,
378
428
  onInvocationFailure: (failure) => {
379
429
  invocationFailures.push(failure);
430
+ if (invocationFailures.length > 64)
431
+ invocationFailures.shift();
380
432
  },
381
433
  emitCollector: emitted,
382
434
  ...(diagnostics ? { diagnostics } : {}),
@@ -407,6 +459,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
407
459
  }
408
460
  }
409
461
  catch (err) {
462
+ const logs = err !== null && typeof err === "object" && "logs" in err
463
+ ? executeLogs(err.logs)
464
+ : undefined;
410
465
  if (err instanceof ExecutorAdmissionError) {
411
466
  if (err.code === "executor_overloaded") {
412
467
  logger.warn("[connecta] execute_code admission rejected", {
@@ -415,6 +470,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
415
470
  });
416
471
  }
417
472
  return failureResponse(err.message, {
473
+ logs,
418
474
  emitted: err instanceof ExecutorExecutionError ? emitted : undefined,
419
475
  diagnostics,
420
476
  code: {
@@ -428,6 +484,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
428
484
  });
429
485
  }
430
486
  return failureResponse(`Executor failed: ${msg(err)}`, {
487
+ logs,
431
488
  emitted,
432
489
  diagnostics,
433
490
  code: "executor_failed",
@@ -440,10 +497,8 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
440
497
  lease?.release();
441
498
  options.signal?.removeEventListener("abort", forwardAbort);
442
499
  }
443
- const logs = outcome.logs && outcome.logs.length > 0
444
- ? truncateExecuteText(outcome.logs.join("\n"), MAX_EXECUTE_LOG_CHARS)
445
- : undefined;
446
- if (outcome.error) {
500
+ const logs = executeLogs(outcome.logs);
501
+ if (outcome.error !== undefined) {
447
502
  // Executor bridges necessarily reduce thrown host errors to strings.
448
503
  // Match that terminal string back to the request-local typed failure so
449
504
  // an unhandled tool failure keeps the same structured contract as
@@ -457,8 +512,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
457
512
  // rather than losing it to prose.
458
513
  let invocationFailure;
459
514
  for (const match of [
460
- (candidate) => [candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
461
- (candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) => message !== undefined && outcome.error?.includes(message) === true),
515
+ (candidate) => outcome.error !== "" &&
516
+ [candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
517
+ (candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) =>
518
+ // E6: empty or tiny prose cannot identify a wrapped failure.
519
+ message !== undefined &&
520
+ message.length >= 8 &&
521
+ outcome.error?.includes(message) === true),
462
522
  ]) {
463
523
  for (let i = invocationFailures.length - 1; i >= 0; i--) {
464
524
  const candidate = invocationFailures[i];
@@ -471,10 +531,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
471
531
  break;
472
532
  }
473
533
  if (invocationFailure) {
474
- // Handed back whole, with no size guard of its own — the failure was
475
- // framed with bounded caller text (`boundedEchoText`) precisely so
476
- // this path never needs one. Adding a cap here instead would leave the
477
- // top-level surfaces, which have the same amplification, uncovered.
534
+ // E1/X11: return the same bounded details the guest received.
478
535
  return failureResponse(invocationFailure.details.message, {
479
536
  logs,
480
537
  emitted,
@@ -482,7 +539,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
482
539
  code: invocationFailure.details,
483
540
  });
484
541
  }
485
- const message = `Error: ${outcome.error}`;
542
+ const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
486
543
  return failureResponse(message, {
487
544
  logs,
488
545
  emitted,
@@ -521,6 +578,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
521
578
  return response;
522
579
  };
523
580
  }
581
+ function executeLogs(value) {
582
+ return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
583
+ ? truncateExecuteText(value.join("\n"), MAX_EXECUTE_LOG_CHARS)
584
+ : undefined;
585
+ }
524
586
  function failureResponse(message, options) {
525
587
  const { logs, emitted, diagnostics, code } = options;
526
588
  if (diagnostics || typeof code !== "string") {
@@ -57,7 +57,9 @@ export function prepareExecuteResultForTransport(outcome) {
57
57
  // characters). Preserve that Executor-level shape; createExecuteTool applies
58
58
  // the smaller model-facing 4k presentation cap in the parent.
59
59
  const logs = outcome.logs && outcome.logs.length > 0 ? outcome.logs : undefined;
60
- if (outcome.error) {
60
+ // An empty string is still a failure (E5): the parent renders a fixed
61
+ // message for it, but this transport must not turn it into a success.
62
+ if (outcome.error !== undefined) {
61
63
  return {
62
64
  result: undefined,
63
65
  error: outcome.error,
@@ -59,7 +59,11 @@ async function run(payload) {
59
59
  activeJobId = payload.id;
60
60
  try {
61
61
  const providers = payload.providers.map(({ name, prelude }) => provider(name, payload.id, prelude));
62
- const raw = await executeQuickJs(payload.code, providers, payload.options);
62
+ const raw = await executeQuickJs(payload.code, providers, payload.options, (entry) => send({
63
+ type: "log",
64
+ jobId: payload.id,
65
+ payloadJson: stringifyBounded(entry, "QuickJS log entry"),
66
+ }));
63
67
  const prepared = {
64
68
  outcome: prepareExecuteResultForTransport(raw),
65
69
  ...(raw.timedOut ? { timedOut: true } : {}),
@@ -29,6 +29,10 @@ export type ChildToParentMessage = {
29
29
  jobId: number;
30
30
  callId: number;
31
31
  payloadJson: string;
32
+ } | {
33
+ type: "log";
34
+ jobId: number;
35
+ payloadJson: string;
32
36
  } | {
33
37
  type: "result";
34
38
  jobId: number;
@@ -22,4 +22,4 @@ export interface QuickJsExecutionResult extends ExecuteResult {
22
22
  * CPU accumulates only while evalCode/executePendingJobs is synchronously
23
23
  * driving QuickJS, so a slow downstream does not consume the short CPU budget.
24
24
  */
25
- export declare function executeQuickJs(code: string, providers: ExecutorProvider[], options: QuickJsRuntimeOptions): Promise<QuickJsExecutionResult>;
25
+ export declare function executeQuickJs(code: string, providers: ExecutorProvider[], options: QuickJsRuntimeOptions, onLog?: (entry: string) => void): Promise<QuickJsExecutionResult>;
@@ -80,30 +80,43 @@ export function normalizeCode(code) {
80
80
  * and fail closed there.
81
81
  */
82
82
  function setupScript(providers) {
83
+ // X11: raw replies carry an authenticated frame. Capture the bridge and
84
+ // codec before guest code runs, and expose only the decoded provider calls.
83
85
  const lines = [
84
- `globalThis.console = (() => {
85
- const fmt = (x) => { if (typeof x === "string") return x; try { return JSON.stringify(x); } catch { return String(x); } };
86
- const emit = (...a) => __log(a.map(fmt).join(" "));
87
- return { log: emit, info: emit, warn: emit, error: emit, debug: emit };
88
- })();`,
89
- `globalThis.__invoke = async (ns, fn, args) => {
90
- const r = JSON.parse(await __call(ns, fn, JSON.stringify(args)));
91
- if (!r.ok) throw new Error(r.error);
92
- return r.value;
93
- };`,
94
- `globalThis.__namespace = (ns) => Object.freeze(new Proxy(Object.create(null), {
95
- get: (_target, key) => typeof key === "string" ? (...args) => __invoke(ns, key, args) : undefined
96
- }));`,
86
+ `(() => {
87
+ const call = globalThis.__call;
88
+ const log = globalThis.__log;
89
+ const parse = JSON.parse;
90
+ const stringify = JSON.stringify;
91
+ const freeze = Object.freeze;
92
+ const ProxyConstructor = Proxy;
93
+ const create = Object.create;
94
+ delete globalThis.__call;
95
+ delete globalThis.__log;
96
+ globalThis.console = (() => {
97
+ const fmt = (x) => { if (typeof x === "string") return x; try { return stringify(x); } catch { return String(x); } };
98
+ const emit = (...a) => log(a.map(fmt).join(" "));
99
+ return { log: emit, info: emit, warn: emit, error: emit, debug: emit };
100
+ })();
101
+ const invoke = async (ns, fn, args) => {
102
+ const r = parse(await call(ns, fn, stringify(args)));
103
+ if (!r.ok) throw new GuestError(r.error);
104
+ return r.value;
105
+ };
106
+ const namespace = (ns) => freeze(new ProxyConstructor(create(null), {
107
+ get: (_target, key) => typeof key === "string" ? (...args) => invoke(ns, key, args) : undefined
108
+ }));`,
97
109
  ];
98
110
  for (const p of providers) {
99
111
  const ns = JSON.stringify(p.name);
100
- lines.push(`globalThis[${ns}] = __namespace(${ns});`);
112
+ lines.push(`globalThis[${ns}] = namespace(${ns});`);
101
113
  }
102
114
  for (const p of providers) {
103
115
  if (p.prelude)
104
116
  lines.push(p.prelude);
105
117
  }
106
- lines.push(`delete globalThis.__namespace;`);
118
+ lines.push(`const GuestError = globalThis.Error;
119
+ })();`);
107
120
  return lines.join("\n");
108
121
  }
109
122
  /** True when a dumped error is QuickJS's deadline-interrupt signal. */
@@ -145,7 +158,7 @@ function waitForHostOrDeadline(waitForSettle, remainingMs) {
145
158
  });
146
159
  });
147
160
  }
148
- function installBridge(ctx, providers, logs) {
161
+ function installBridge(ctx, providers, logs, onLog) {
149
162
  const bridge = {
150
163
  pending: 0,
151
164
  aborted: false,
@@ -156,13 +169,17 @@ function installBridge(ctx, providers, logs) {
156
169
  // Keep both the in-memory character budget and the twice-JSON-encoded
157
170
  // transport budget. Reserve enough byte budget for whichever truncation
158
171
  // marker ends the stream.
172
+ const captureLog = (entry) => {
173
+ logs.push(entry);
174
+ onLog?.(entry);
175
+ };
159
176
  let logTotalChars = 0;
160
177
  let logTotalTransportBytes = 0;
161
178
  let logBudgetSpent = false;
162
179
  const logFn = ctx.newFunction("__log", (h) => {
163
180
  if (logs.length >= MAX_LOG_ENTRIES) {
164
181
  if (logs.length === MAX_LOG_ENTRIES) {
165
- logs.push(LOG_ENTRY_LIMIT_MARKER);
182
+ captureLog(LOG_ENTRY_LIMIT_MARKER);
166
183
  }
167
184
  return;
168
185
  }
@@ -178,11 +195,11 @@ function installBridge(ctx, providers, logs) {
178
195
  if (logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS ||
179
196
  logTotalTransportBytes + entryTransportBytes >
180
197
  MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES) {
181
- logs.push(LOG_SIZE_LIMIT_MARKER);
198
+ captureLog(LOG_SIZE_LIMIT_MARKER);
182
199
  logBudgetSpent = true;
183
200
  return;
184
201
  }
185
- logs.push(entry);
202
+ captureLog(entry);
186
203
  logTotalChars += entry.length;
187
204
  logTotalTransportBytes += entryTransportBytes;
188
205
  });
@@ -248,7 +265,7 @@ function installBridge(ctx, providers, logs) {
248
265
  * CPU accumulates only while evalCode/executePendingJobs is synchronously
249
266
  * driving QuickJS, so a slow downstream does not consume the short CPU budget.
250
267
  */
251
- export async function executeQuickJs(code, providers, options) {
268
+ export async function executeQuickJs(code, providers, options, onLog) {
252
269
  const { timeoutMs, cpuTimeMs, memoryLimitBytes, maxStackSizeBytes } = options;
253
270
  const QuickJS = await getQuickJS();
254
271
  const ctx = QuickJS.newContext();
@@ -282,7 +299,7 @@ export async function executeQuickJs(code, providers, options) {
282
299
  return wallInterrupted || cpuInterrupted;
283
300
  });
284
301
  const logs = [];
285
- const bridge = installBridge(ctx, providers, logs);
302
+ const bridge = installBridge(ctx, providers, logs, onLog);
286
303
  const finish = (r) => {
287
304
  bridge.aborted = true;
288
305
  // Outstanding host calls still hold deferred-promise handles; their
@@ -7,6 +7,7 @@ import { existsSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { AdmissionController, ExecutorAdmissionError, ExecutorExecutionError, } from "../executor-admission.js";
9
9
  import { msg } from "../errors.js";
10
+ import { MAX_EXECUTE_LOG_CHARS } from "../executor-result.js";
10
11
  import { hostCallLabel, MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
11
12
  export { normalizeCode } from "./quickjs-runtime.js";
12
13
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -55,9 +56,14 @@ function nonNegativeWhole(value, fallback, name) {
55
56
  return resolved;
56
57
  }
57
58
  function errorPayload(error) {
59
+ // X11: a partial authenticated frame exposes its secret instead of decoding.
60
+ // Refuse it whole if a host ever bypasses execute.ts's framing bound.
61
+ const message = error.length > MAX_ERROR_CHARS && error.includes("\u001econnecta-error:")
62
+ ? `Host failure exceeded the ${MAX_ERROR_CHARS}-character bridge limit.`
63
+ : error.slice(0, MAX_ERROR_CHARS);
58
64
  return JSON.stringify({
59
65
  ok: false,
60
- error: error.slice(0, MAX_ERROR_CHARS),
66
+ error: message,
61
67
  });
62
68
  }
63
69
  function delay(ms, signal) {
@@ -217,14 +223,13 @@ class QuickJsChildPool {
217
223
  child.channel?.ref();
218
224
  return new Promise((resolve, reject) => {
219
225
  const wallTimer = setTimeout(() => {
220
- this.resolveActive(slot, {
221
- result: undefined,
222
- error: `QuickJS child exceeded the ${this.runtimeOptions.timeoutMs}ms wall budget and was terminated.`,
223
- });
226
+ this.rejectActive(slot, new Error(`QuickJS child exceeded the ${this.runtimeOptions.timeoutMs}ms wall budget and was terminated.`));
224
227
  this.recycle(slot);
225
228
  }, this.runtimeOptions.timeoutMs + CHILD_EXIT_GRACE_MS);
226
229
  const active = {
227
230
  id,
231
+ logs: [],
232
+ logChars: 0,
228
233
  providers: providerMap,
229
234
  resolve,
230
235
  reject,
@@ -328,7 +333,7 @@ class QuickJsChildPool {
328
333
  return;
329
334
  this.recordCrash(slot);
330
335
  const error = childExitError(`QuickJS child exited unexpectedly (${exitDescription}).`, stderrTail);
331
- this.resolveActive(slot, { result: undefined, error: error.message });
336
+ this.rejectActive(slot, error);
332
337
  });
333
338
  child.unref();
334
339
  child.channel?.unref();
@@ -350,19 +355,39 @@ class QuickJsChildPool {
350
355
  return;
351
356
  if (typeof message.payloadJson !== "string")
352
357
  return;
358
+ if (message.type === "log") {
359
+ if (message.jobId !== active.id)
360
+ return;
361
+ // Keep one extra character so the presentation layer can signal loss.
362
+ // The final reply owns complete successful logs; this prefix survives
363
+ // only when that reply cannot arrive. Bound even a compromised child.
364
+ if (active.logChars >= MAX_EXECUTE_LOG_CHARS + 1)
365
+ return;
366
+ if (serializedBytes(message.payloadJson) > MAX_QUICKJS_IPC_BYTES)
367
+ return;
368
+ try {
369
+ stringifyBounded(message, "QuickJS log IPC envelope");
370
+ const entry = JSON.parse(message.payloadJson);
371
+ if (typeof entry !== "string")
372
+ return;
373
+ const separator = active.logs.length > 0 ? 1 : 0;
374
+ const retained = entry.slice(0, MAX_EXECUTE_LOG_CHARS + 1 - active.logChars - separator);
375
+ active.logs.push(retained);
376
+ active.logChars += separator + retained.length;
377
+ }
378
+ catch { /* Malformed log messages carry no output. */ }
379
+ return;
380
+ }
353
381
  if (message.type === "host-call") {
354
382
  if (message.jobId !== active.id)
355
383
  return;
356
- await this.handleHostCall(child, active, message);
384
+ await this.handleHostCall(slot, child, active, message);
357
385
  return;
358
386
  }
359
387
  if (message.type !== "result" || message.jobId !== active.id)
360
388
  return;
361
389
  if (serializedBytes(message.payloadJson) > MAX_QUICKJS_IPC_BYTES) {
362
- this.resolveActive(slot, {
363
- result: undefined,
364
- error: "QuickJS execution result exceeded the IPC limit.",
365
- });
390
+ this.rejectActive(slot, new Error("QuickJS execution result exceeded the IPC limit."));
366
391
  this.recycle(slot);
367
392
  return;
368
393
  }
@@ -380,14 +405,11 @@ class QuickJsChildPool {
380
405
  }
381
406
  }
382
407
  catch (err) {
383
- this.resolveActive(slot, {
384
- result: undefined,
385
- error: `QuickJS child returned an invalid result: ${msg(err)}`,
386
- });
408
+ this.rejectActive(slot, new Error(`QuickJS child returned an invalid result: ${msg(err)}`));
387
409
  this.recycle(slot);
388
410
  }
389
411
  }
390
- async handleHostCall(child, active, message) {
412
+ async handleHostCall(slot, child, active, message) {
391
413
  let payloadJson;
392
414
  try {
393
415
  if (serializedBytes(message.payloadJson) >
@@ -422,35 +444,54 @@ class QuickJsChildPool {
422
444
  catch (err) {
423
445
  payloadJson = errorPayload(msg(err));
424
446
  }
425
- if (!child.connected)
447
+ if (!child.connected || slot.active !== active)
426
448
  return;
449
+ const response = {
450
+ type: "host-result",
451
+ jobId: message.jobId,
452
+ callId: message.callId,
453
+ payloadJson,
454
+ };
427
455
  try {
428
- const response = {
429
- type: "host-result",
430
- jobId: message.jobId,
431
- callId: message.callId,
432
- payloadJson,
433
- };
434
456
  stringifyBounded(response, "QuickJS host-result IPC envelope");
435
- child.send(response);
436
457
  }
437
458
  catch {
438
- // The execution has already ended or the child is exiting. Its exit
439
- // handler owns the structured failure for any still-active job.
459
+ // L6/X10: settle the rejected call even if the outer encoding overflows.
460
+ response.payloadJson = errorPayload(`QuickJS host-result IPC envelope could not be serialized within the ${MAX_QUICKJS_IPC_BYTES}-byte IPC limit.`);
461
+ }
462
+ const failedSend = (error) => {
463
+ if (!error || slot.active !== active)
464
+ return;
465
+ this.rejectActive(slot, error);
466
+ this.recycle(slot);
467
+ };
468
+ try {
469
+ stringifyBounded(response, "QuickJS host-result IPC envelope");
470
+ child.send(response, failedSend);
471
+ }
472
+ catch (err) {
473
+ failedSend(err instanceof Error ? err : new Error(msg(err)));
440
474
  }
441
475
  }
442
476
  resolveActive(slot, outcome) {
443
477
  const active = slot.active;
444
478
  if (!active)
445
479
  return;
480
+ // Never concatenate the stream with the final reply: those entries are
481
+ // the same logs. A normal reply retains its existing full log contract.
482
+ const resolved = outcome.logs === undefined && active.logs.length > 0
483
+ ? { ...outcome, logs: active.logs }
484
+ : outcome;
446
485
  this.clearActive(slot, active);
447
- active.resolve(outcome);
486
+ active.resolve(resolved);
448
487
  }
449
488
  rejectActive(slot, error) {
450
489
  const active = slot.active;
451
490
  if (!active)
452
491
  return;
453
492
  this.clearActive(slot, active);
493
+ if (active.logs.length > 0)
494
+ Object.assign(error, { logs: active.logs });
454
495
  active.reject(error);
455
496
  }
456
497
  clearActive(slot, active) {
package/dist/index.d.ts CHANGED
@@ -51,6 +51,13 @@ export interface ConnectaCallsConfig {
51
51
  */
52
52
  maxResultBytes?: number;
53
53
  }
54
+ /** Runtime-wide bounds for transient direct-call result paging. */
55
+ export interface ConnectaResultsConfig {
56
+ /** Stored bytes, including the paging envelope. Default 8 MiB. Zero disables stashing. */
57
+ maxStashBytes?: number;
58
+ /** Stored or in-flight entries. Default 64. Zero disables stashing. */
59
+ maxStashEntries?: number;
60
+ }
54
61
  /** Budgets for execute_code programs: host calls and rich output (`connecta.emit`). */
55
62
  export interface ConnectaExecuteConfig {
56
63
  /**
@@ -149,6 +156,11 @@ export interface ConnectaConfig {
149
156
  * HTTPS URL also redirects matching inbound HTTP requests to HTTPS.
150
157
  */
151
158
  publicUrl?: string;
159
+ /**
160
+ * Exact browser MCP origins, or "*". Defaults to publicUrl's origin and
161
+ * HTTP(S) loopback origins at any port. Originless clients are admitted.
162
+ */
163
+ allowedOrigins?: readonly string[] | "*";
152
164
  /** Optional recorder and reader, created by activityHistory() from /activity. */
153
165
  activity?: ActivityModule;
154
166
  /** Replaceable owner-partitioned credential storage. Omit for config-owned secrets. */
@@ -159,6 +171,8 @@ export interface ConnectaConfig {
159
171
  discovery?: ConnectaDiscoveryConfig;
160
172
  /** Deployment-wide call deadlines and result paging threshold. */
161
173
  calls?: ConnectaCallsConfig;
174
+ /** Runtime-wide transient result stash limits, shared across subjects. */
175
+ results?: ConnectaResultsConfig;
162
176
  /** Budgets for the `connecta.emit` rich-output channel in execute_code. */
163
177
  execute?: ConnectaExecuteConfig;
164
178
  /** Bounded MCP and fallback code-mode admission. */
@@ -208,5 +222,5 @@ export { CONNECTA_VERSION } from "./version.js";
208
222
  export type { Registry } from "./registry.js";
209
223
  export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
210
224
  export type { ApiOptions, ApiTool } from "./connectors/api.js";
211
- export type { CatalogDriftCounts, CatalogDriftReport, ConnectaBranding, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
225
+ export type { CatalogDriftCounts, CatalogDriftReport, ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
212
226
  export type { ActivityActor, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, CatalogDriftActivityEvent, ToolCallActivityEvent, } from "./activity.js";