@zackbart/connecta 0.29.0 → 0.30.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +123 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/catalog-cache.js +2 -2
  9. package/dist/catalog-service.js +103 -63
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +12 -7
  12. package/dist/connectors/guarded-fetch.js +3 -3
  13. package/dist/connectors/negotiation-cache.js +1 -1
  14. package/dist/downstream-input-context.js +9 -0
  15. package/dist/downstream-input.js +13 -11
  16. package/dist/errors.d.ts +36 -12
  17. package/dist/errors.js +19 -9
  18. package/dist/execute.js +375 -43
  19. package/dist/executor-result.js +11 -9
  20. package/dist/executors/quickjs-child.js +12 -5
  21. package/dist/executors/quickjs-protocol.js +5 -6
  22. package/dist/executors/quickjs-runtime.js +6 -1
  23. package/dist/executors/quickjs.js +20 -4
  24. package/dist/guest-runtime.js +52 -2
  25. package/dist/guest-types.d.ts +47 -5
  26. package/dist/index.d.ts +1 -1
  27. package/dist/invocation.js +40 -14
  28. package/dist/meta-output.js +0 -15
  29. package/dist/meta-tools.js +166 -95
  30. package/dist/oauth-handoff.js +88 -11
  31. package/dist/program-result.js +5 -0
  32. package/dist/program-writes.js +5 -1
  33. package/dist/provider.d.ts +1 -1
  34. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  35. package/dist/providers/_shared/request-token.js +83 -0
  36. package/dist/providers/infisical/index.d.ts +7 -0
  37. package/dist/providers/infisical/index.js +713 -0
  38. package/dist/providers/infisical/skill.generated.js +8 -0
  39. package/dist/registry.d.ts +8 -6
  40. package/dist/registry.js +134 -61
  41. package/dist/resource-read.js +4 -1
  42. package/dist/routes/mcp.js +11 -2
  43. package/dist/sent-secrets.d.ts +54 -13
  44. package/dist/sent-secrets.js +770 -231
  45. package/dist/skills.js +26 -16
  46. package/dist/sqlite.js +21 -6
  47. package/dist/storage/keys.js +20 -5
  48. package/dist/storage/memory.js +9 -4
  49. package/dist/storage/sql.js +18 -9
  50. package/dist/types.d.ts +17 -3
  51. package/dist/usage-guide.js +49 -40
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/worker.js +2 -1
  55. package/documentation/README.md +63 -0
  56. package/documentation/architecture.md +121 -89
  57. package/documentation/auth.md +138 -5
  58. package/documentation/code-mode.md +224 -107
  59. package/documentation/deploying.md +146 -0
  60. package/documentation/integrating.md +159 -0
  61. package/documentation/meta-tools.md +135 -30
  62. package/documentation/operating.md +135 -0
  63. package/documentation/operator-tests.md +4 -0
  64. package/documentation/operator-ui.md +132 -0
  65. package/documentation/provider-migration-0.29.md +5 -0
  66. package/examples/worker/README.md +7 -0
  67. package/package.json +7 -1
  68. package/templates/node/README.md +7 -0
  69. package/templates/node/package.json +1 -1
package/dist/execute.js CHANGED
@@ -5,8 +5,9 @@ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
5
5
  import { z } from "zod";
6
6
  import { advertisedSchema } from "./advertised-schema.js";
7
7
  import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
8
- import { createMetaTools, jsonResult } from "./meta-tools.js";
9
- import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText, } from "./executor-result.js";
8
+ import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
9
+ import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
10
+ import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
10
11
  import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
11
12
  import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
12
13
  import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
@@ -118,6 +119,10 @@ class ExecuteDiagnostics {
118
119
  };
119
120
  }
120
121
  }
122
+ /**
123
+ * Guest-authored blocks exclude pointers (INV-3). Native call blocks are
124
+ * retained on the host and forwarded only through request-local references.
125
+ */
121
126
  const EMIT_SHAPE_HINT = '{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
122
127
  /**
123
128
  * Strict M1 validation: required fields present and string-valued, nothing
@@ -175,26 +180,142 @@ export class EmitCollector {
175
180
  this.diagnostics = diagnostics;
176
181
  }
177
182
  accept(raw) {
178
- const block = requireEmittedBlock(raw);
179
- if (this.blocks.length >= this.maxBlocks) {
180
- // M5: distinguish exhausted block slots from the remaining byte budget.
181
- 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`);
182
- }
183
- const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
184
- if (this.bytes + size > this.maxBytes) {
185
- throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
186
- }
187
- const textChars = block.type === "text" ? JSON.stringify(block).length : 0;
188
- // Reserve space for a reduced result envelope even if text fills the budget.
189
- if (this.textChars + textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
190
- throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
183
+ this.acceptNative([requireEmittedBlock(raw)]);
184
+ }
185
+ /** Only the host's retained downstream result may enter this path. */
186
+ acceptNative(blocks) {
187
+ let bytes = this.bytes;
188
+ let textChars = this.textChars;
189
+ let count = this.blocks.length;
190
+ for (const block of blocks) {
191
+ if (count >= this.maxBlocks) {
192
+ throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - bytes} of ${this.maxBytes} serialized bytes remaining`);
193
+ }
194
+ const serialized = JSON.stringify(block);
195
+ const size = diagnosticsEncoder.encode(serialized).byteLength;
196
+ if (bytes + size > this.maxBytes) {
197
+ throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - bytes} of ${this.maxBytes} remaining`);
198
+ }
199
+ textChars += block.type === "text" ? serialized.length : 0;
200
+ // Embedded resource text shares the result cap too; binary blobs use
201
+ // the transport budget, just like images and audio.
202
+ if (block.type === "resource" && "text" in block.resource)
203
+ textChars += serialized.length;
204
+ if (textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
205
+ throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
206
+ }
207
+ count++;
208
+ bytes += size;
191
209
  }
192
- this.textChars += textChars;
193
- this.blocks.push(block);
194
- this.bytes += size;
210
+ this.textChars = textChars;
211
+ this.bytes = bytes;
212
+ this.blocks.push(...blocks);
195
213
  this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
196
214
  }
197
215
  }
216
+ /** Bound both JSON size and object amplification before retaining native blocks. */
217
+ class NativePresentationLimit extends Error {
218
+ }
219
+ function boundedNativeValue(value, maxBytes, copy) {
220
+ let bytes = 0;
221
+ let nodes = 0;
222
+ // Empty objects cost only two JSON bytes but much more heap. Derive the
223
+ // allocation ceiling from the transport budget, with a fixed stack ceiling.
224
+ const maxNodes = Math.floor(Math.min(maxBytes, PROGRAM_RESULT_INLINE_BYTES) / 32);
225
+ const active = new Set();
226
+ const charge = (size) => {
227
+ bytes += size;
228
+ if (bytes > maxBytes)
229
+ throw new NativePresentationLimit();
230
+ };
231
+ const string = (text) => {
232
+ // Count JSON escapes and UTF-8 directly, without allocating a potentially
233
+ // six-times-larger escaped string or byte buffer just to reject it.
234
+ if (text.length > maxBytes - bytes)
235
+ throw new NativePresentationLimit();
236
+ charge(2);
237
+ for (let i = 0; i < text.length; i++) {
238
+ const unit = text.charCodeAt(i);
239
+ if (unit === 34 || unit === 92 || unit === 8 || unit === 9 || unit === 10 || unit === 12 || unit === 13)
240
+ charge(2);
241
+ else if (unit < 32)
242
+ charge(6);
243
+ else if (unit < 128)
244
+ charge(1);
245
+ else if (unit < 2048)
246
+ charge(2);
247
+ else if (unit >= 0xd800 &&
248
+ unit <= 0xdbff &&
249
+ text.charCodeAt(i + 1) >= 0xdc00 &&
250
+ text.charCodeAt(i + 1) <= 0xdfff) {
251
+ charge(4);
252
+ i++;
253
+ }
254
+ else
255
+ charge(unit >= 0xd800 && unit <= 0xdfff ? 6 : 3);
256
+ }
257
+ };
258
+ const visit = (item, depth) => {
259
+ if (++nodes > maxNodes || depth > 64)
260
+ throw new NativePresentationLimit();
261
+ if (typeof item === "string") {
262
+ string(item);
263
+ return item;
264
+ }
265
+ if (item === null || typeof item === "boolean" || typeof item === "number") {
266
+ charge(JSON.stringify(item).length);
267
+ return item;
268
+ }
269
+ if (typeof item !== "object" || active.has(item))
270
+ throw new NativePresentationLimit();
271
+ const array = Array.isArray(item);
272
+ if (![array ? Array.prototype : Object.prototype, null].includes(Object.getPrototypeOf(item)))
273
+ throw new NativePresentationLimit();
274
+ const jsonHook = Object.getOwnPropertyDescriptor(item, "toJSON");
275
+ if (jsonHook && (!("value" in jsonHook) || typeof jsonHook.value === "function"))
276
+ throw new NativePresentationLimit();
277
+ active.add(item);
278
+ charge(2);
279
+ if (array) {
280
+ if (item.length > maxNodes - nodes)
281
+ throw new NativePresentationLimit();
282
+ const result = copy ? [] : undefined;
283
+ for (let i = 0; i < item.length; i++) {
284
+ if (i)
285
+ charge(1);
286
+ const descriptor = Object.getOwnPropertyDescriptor(item, String(i));
287
+ if (descriptor && !("value" in descriptor))
288
+ throw new NativePresentationLimit();
289
+ const child = visit(descriptor?.value ?? null, depth + 1);
290
+ result?.push(child);
291
+ }
292
+ active.delete(item);
293
+ return result;
294
+ }
295
+ const result = copy ? {} : undefined;
296
+ let first = true;
297
+ for (const key in item) {
298
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
299
+ if (!descriptor?.enumerable)
300
+ continue;
301
+ if (!("value" in descriptor))
302
+ throw new NativePresentationLimit();
303
+ if (descriptor.value === undefined)
304
+ continue;
305
+ if (!first)
306
+ charge(1);
307
+ first = false;
308
+ string(key);
309
+ charge(1);
310
+ const child = visit(descriptor.value, depth + 1);
311
+ if (result)
312
+ Object.defineProperty(result, key, { value: child, enumerable: true, writable: true, configurable: true });
313
+ }
314
+ active.delete(item);
315
+ return result;
316
+ };
317
+ return { value: visit(value, 0), bytes };
318
+ }
198
319
  /** A positive whole-number budget, or the default when the value is unusable. */
199
320
  function resolveBudget(value, fallback) {
200
321
  return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
@@ -320,6 +441,20 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
320
441
  stopped.catch(() => { });
321
442
  const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
322
443
  let writes = 0;
444
+ // These are transfer cursors only. All result bytes live in the existing stash.
445
+ const transfers = new Map();
446
+ const nativeResults = new Map();
447
+ const transportResult = (value) => {
448
+ if (value.format === "paged") {
449
+ const handle = value;
450
+ if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
451
+ const transfer = `transfer:${crypto.randomUUID()}`;
452
+ transfers.set(transfer, { id: handle.resultId, offset: 0 });
453
+ return { transfer, handle };
454
+ }
455
+ }
456
+ return value;
457
+ };
323
458
  /** Budget and account for writes admitted by the pool trust decision. */
324
459
  const invocationContext = (sending, timeoutMs) => ({
325
460
  source: "execute_code",
@@ -329,6 +464,86 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
329
464
  ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
330
465
  unwrapResult: true,
331
466
  trust: limits.trust,
467
+ processResult: async (value, resolved, secrets, format, raw) => {
468
+ const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, limits.trust, requestScope, wireBytes);
469
+ const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
470
+ ? raw
471
+ : undefined;
472
+ const dataKey = envelope
473
+ ? "toolResult" in envelope
474
+ ? "toolResult"
475
+ : envelope.structuredContent !== undefined
476
+ ? "structuredContent"
477
+ : undefined
478
+ : undefined;
479
+ // Preserve #788 before even inspecting presentation. Paging serializes
480
+ // the structured value synchronously; no native metadata is visited.
481
+ if (dataKey !== undefined &&
482
+ diagnosticsEncoder.encode(JSON.stringify(value)).byteLength > PROGRAM_RESULT_INLINE_BYTES) {
483
+ return page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
484
+ }
485
+ const content = envelope?.content;
486
+ if (!Array.isArray(content) ||
487
+ !content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
488
+ return transportResult(await page(value, format));
489
+ }
490
+ const unavailablePresentation = () => ({
491
+ format: "paged",
492
+ valueFormat: "json",
493
+ totalBytes: 0,
494
+ truncated: true,
495
+ hint: "Paging is unavailable. Native presentation exceeds byte, node or depth limits, or is not acyclic JSON. Reduce the downstream presentation before forwarding it.",
496
+ });
497
+ let snapshot;
498
+ let retained;
499
+ try {
500
+ // Oversized but bounded envelopes may still use the existing stash.
501
+ // Measure without copying them; only inline native blocks are retained.
502
+ const measured = boundedNativeValue(envelope, Math.max(EXECUTE_MAX_EMITTED_BYTES, limits.emitCollector?.maxBytes ?? 0), false);
503
+ if (measured.bytes <= PROGRAM_RESULT_INLINE_BYTES) {
504
+ retained = boundedNativeValue(content, PROGRAM_RESULT_INLINE_BYTES, true).value;
505
+ snapshot = { ...envelope, content: retained };
506
+ }
507
+ else {
508
+ snapshot = envelope;
509
+ }
510
+ }
511
+ catch (error) {
512
+ if (!(error instanceof NativePresentationLimit))
513
+ throw error;
514
+ const result = dataKey === undefined ? undefined : transportResult(await page(value, format));
515
+ const notice = unavailablePresentation();
516
+ return result ? { value: result, native: { value: notice } } : notice;
517
+ }
518
+ // Snapshot blocks before either stash operation yields. Start both
519
+ // serializations now so an oversized presentation's pages also own the
520
+ // original bytes even if the structured-data stash pauses.
521
+ const resultPending = dataKey === undefined ? undefined : page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
522
+ const nativePending = page(snapshot, "json");
523
+ const result = await resultPending;
524
+ const native = await nativePending;
525
+ let valueResult;
526
+ if (native.format === "paged") {
527
+ valueResult = result ? transportResult(result) : undefined;
528
+ if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
529
+ return valueResult ? { value: valueResult, native: { value: native } } : native;
530
+ }
531
+ }
532
+ // Redaction can shrink an oversized original below the inline cap. It
533
+ // still has no retained snapshot: keep its handle, never mint a ref to
534
+ // absent or connector-owned blocks.
535
+ if (!retained) {
536
+ const notice = native.format === "paged" ? native : unavailablePresentation();
537
+ const valueResult = result ? transportResult(result) : undefined;
538
+ return valueResult ? { value: valueResult, native: { value: notice } } : notice;
539
+ }
540
+ const ref = crypto.randomUUID();
541
+ nativeResults.set(ref, retained);
542
+ return {
543
+ ...(valueResult ? { value: valueResult } : {}),
544
+ native: { ref, value: transportResult(native), dataKey },
545
+ };
546
+ },
332
547
  beforeWrite: (target) => Effect.sync(() => {
333
548
  if (programWrites?.isClosed) {
334
549
  return {
@@ -391,15 +606,36 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
391
606
  hostCallTimeoutMs;
392
607
  const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
393
608
  // A trusted-pool write settles here — unknown if the call never returned an
394
- // outcome.
609
+ // outcome. Credentials are filtered from arguments; submitted private values are not.
395
610
  const sending = {};
396
611
  const outcome = yield* invocation
397
- .pipeline(address, sentSecrets.redact(args ?? {}), invocationContext(sending, timeoutMs))
612
+ .pipeline(address, sentSecrets.redactCredentials(args ?? {}), invocationContext(sending, timeoutMs))
398
613
  .pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
614
+ if (outcome.resolved) {
615
+ const resolved = outcome.resolved;
616
+ limits.resultDependencies?.push({
617
+ connector: resolved.connector.id,
618
+ tool: resolved.definition.name,
619
+ classification: outcome.dispatched && resolved.definition.classification !== "read" ? "write" : "read",
620
+ // A refusal exposes catalog metadata, not a completed write's result.
621
+ ...(!outcome.dispatched ? { access: "catalog" } : {}),
622
+ });
623
+ }
624
+ else {
625
+ // Resolution failures can expose a catalog error or connector metadata
626
+ // without ever finding a tool. Such bytes depend on connector visibility,
627
+ // not on the existence of the requested (possibly unknown) tool.
628
+ const target = registry.resolveAddress(address);
629
+ if (target)
630
+ catalogDependency(target.connector.id);
631
+ if (!outcome.ok)
632
+ for (const connector of outcome.error.configuredConnectors ?? [])
633
+ catalogDependency(connector);
634
+ }
399
635
  diagnostics?.recordCall(outcome);
400
- return outcome.ok
401
- ? { data: outcome.value, format: outcome.format }
402
- : yield* Effect.fail(new InvocationFailure(outcome.error));
636
+ if (!outcome.ok)
637
+ return yield* Effect.fail(new InvocationFailure(outcome.error));
638
+ return outcome.value;
403
639
  });
404
640
  // Discovery gets the same treatment from here (L2): once the run has
405
641
  // ended no search or describe starts, and one still in flight fails
@@ -419,24 +655,58 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
419
655
  ? Effect.fail(cancelled())
420
656
  : Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
421
657
  });
422
- const meta = createMetaTools(registry, baseUrl, { trust: limits.trust, requestScope });
658
+ const meta = createMetaTools(registry, baseUrl, {
659
+ trust: limits.trust,
660
+ requestScope,
661
+ onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
662
+ });
663
+ const catalogDependency = (connector, tool) => {
664
+ if (registry.getConnector(connector))
665
+ limits.resultDependencies?.push({
666
+ connector,
667
+ ...(tool ? { tool } : {}),
668
+ classification: "read",
669
+ access: "catalog",
670
+ });
671
+ };
672
+ const catalogAddressDependency = (address) => {
673
+ const resolved = registry.resolveAddress(address);
674
+ if (resolved)
675
+ catalogDependency(resolved.connector.id, resolved.toolName);
676
+ };
423
677
  const operations = {
424
678
  call,
425
679
  result: (id, options, signal) => Effect.tryPromise({
426
680
  try: async () => {
681
+ const transfer = typeof id === "string" ? transfers.get(id) : undefined;
682
+ const transferId = id;
683
+ if (transfer) {
684
+ id = transfer.id;
685
+ options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
686
+ }
687
+ else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
688
+ id = id.resultId;
689
+ }
427
690
  if (typeof id !== "string" ||
428
691
  (!options && options !== undefined) ||
429
692
  (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
430
- throw guestFailure("invalid_args", "Use connecta.result(id, { offset?, maxBytes? }).");
693
+ throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }).");
431
694
  }
432
- const args = (options ?? {});
433
- if (Object.keys(args).some((key) => !["offset", "maxBytes"].includes(key)) ||
695
+ const args = { ...(options ?? {}) };
696
+ if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
697
+ (args.page !== undefined &&
698
+ (typeof args.page !== "number" ||
699
+ !Number.isSafeInteger(args.page) ||
700
+ args.page < 0 ||
701
+ args.offset !== undefined)) ||
434
702
  (args.offset !== undefined &&
435
703
  (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
436
704
  (args.maxBytes !== undefined &&
437
705
  (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
438
- throw guestFailure("invalid_args", "Use connecta.result(id, { offset?, maxBytes? }) with whole byte offsets >= 0 and maxBytes >= 1.");
706
+ throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive.");
439
707
  }
708
+ // A page must fit every shipped bridge even when the connector cap is raised.
709
+ args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
440
710
  const page = await meta.readResult({ id, ...args }, { signal: signal });
441
711
  if (page.isError)
442
712
  throw new InvocationFailure((page.structuredContent?.error ?? {
@@ -444,6 +714,12 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
444
714
  message: page.content[0]?.text ?? "Result unavailable.",
445
715
  retryable: false,
446
716
  }));
717
+ if (transfer) {
718
+ if (page.structuredContent?.hasMore)
719
+ transfer.offset = page.structuredContent.nextOffset;
720
+ else
721
+ transfers.delete(transferId);
722
+ }
447
723
  return page.structuredContent;
448
724
  },
449
725
  catch: (err) => err,
@@ -455,6 +731,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
455
731
  const skills = new SkillsRegistry(registry, baseUrl, {
456
732
  requestScope,
457
733
  requestSignal: utilitySignal ?? hostAccessSignal,
734
+ onRead: (connector, source) => limits.resultDependencies?.push({
735
+ connector: connector.id,
736
+ classification: "read",
737
+ ...(source === "local" ? { access: "catalog" } : {}),
738
+ }),
458
739
  probeTimeoutMs: limits.probeTimeoutMs,
459
740
  defer: limits.defer,
460
741
  });
@@ -470,6 +751,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
470
751
  requestScope,
471
752
  sentSecrets,
472
753
  timeoutMs: hostCallTimeoutMs,
754
+ onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
473
755
  onConnectorTime: (elapsed) => {
474
756
  connectorMs += elapsed;
475
757
  },
@@ -485,7 +767,26 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
485
767
  if (!limits.emitCollector) {
486
768
  throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
487
769
  }
488
- limits.emitCollector.accept(sentSecrets.redact(block));
770
+ if (block !== null && typeof block === "object" && "contentResult" in block) {
771
+ throw guestFailure("result_too_large", "Native content is not inline; page result.contentResult with connecta.result and reduce it before emitting.");
772
+ }
773
+ if (block !== null && typeof block === "object" && !Array.isArray(block) && "ref" in block) {
774
+ const reference = block;
775
+ const native = typeof reference.ref === "string" ? nativeResults.get(reference.ref) : undefined;
776
+ if (!native ||
777
+ Object.keys(reference).some((key) => !["ref", "index"].includes(key)) ||
778
+ (reference.index !== undefined &&
779
+ (typeof reference.index !== "number" ||
780
+ !Number.isSafeInteger(reference.index) ||
781
+ reference.index < 0 ||
782
+ reference.index >= native.length))) {
783
+ throw guestFailure("invalid_args", "connecta.emit requires a native result from this program's connecta.call.");
784
+ }
785
+ const blocks = reference.index === undefined ? native : [native[reference.index]];
786
+ limits.emitCollector.acceptNative(sentSecrets.redact(blocks));
787
+ }
788
+ else
789
+ limits.emitCollector.accept(sentSecrets.redact(block));
489
790
  },
490
791
  catch: (err) => err,
491
792
  }),
@@ -497,12 +798,35 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
497
798
  includeSchemaKeys: args.includeSchemaKeys !== false,
498
799
  }));
499
800
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
801
+ for (const tool of result.tools)
802
+ catalogAddressDependency(tool.address);
803
+ for (const error of result.catalogErrors)
804
+ catalogDependency(error.connector);
805
+ for (const connector of result.absence?.configuredConnectors ?? [])
806
+ catalogDependency(connector);
807
+ for (const connector of result.queryAnalysis?.configuredConnectors ?? [])
808
+ catalogDependency(connector);
809
+ if (result.queryAnalysis?.connectorScope)
810
+ catalogDependency(result.queryAnalysis.connectorScope);
500
811
  return result;
501
812
  }),
502
813
  describe: (raw) => discovery("describe", async () => {
503
814
  const args = (raw ?? {});
504
815
  const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
505
816
  boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
817
+ for (const tool of result.tools) {
818
+ if (!tool.error)
819
+ catalogAddressDependency(tool.address);
820
+ else {
821
+ const resolved = registry.resolveAddress(tool.address);
822
+ if (resolved)
823
+ catalogDependency(resolved.connector.id);
824
+ for (const connector of tool.errorDetails?.configuredConnectors ?? [])
825
+ catalogDependency(connector);
826
+ for (const suggestion of tool.errorDetails?.suggestions ?? [])
827
+ catalogAddressDependency(suggestion);
828
+ }
829
+ }
506
830
  return result;
507
831
  }),
508
832
  };
@@ -511,7 +835,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
511
835
  const typedFailure = (err) => Effect.suspend(() => {
512
836
  if (!(err instanceof InvocationFailure))
513
837
  return Effect.fail(err);
514
- const failure = boundedGuestFailure(sentSecrets.redact(err));
838
+ const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
515
839
  return Effect.fail(failure);
516
840
  });
517
841
  return {
@@ -534,7 +858,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
534
858
  }
535
859
  // L4/M7: spend on entry, before even invalid arguments are checked;
536
860
  // emit has a separate budget and never spends this one.
537
- const counted = name !== "emit";
861
+ // Transparent transfer pages consume the already-admitted call, not guest host calls.
862
+ const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
538
863
  if (counted && ++hostCalls.attempted > maxHostCalls) {
539
864
  hostCalls.failed++;
540
865
  budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
@@ -703,6 +1028,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
703
1028
  const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
704
1029
  const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
705
1030
  const programWrites = new ProgramWrites();
1031
+ const resultDependencies = [];
706
1032
  const dispatchController = new AbortController();
707
1033
  const terminal = Deferred.makeUnsafe();
708
1034
  let budgetFailure;
@@ -754,6 +1080,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
754
1080
  trust: config.trust,
755
1081
  maxWrites: config.maxWrites,
756
1082
  programWrites,
1083
+ resultDependencies,
757
1084
  dispatchController,
758
1085
  }, requestScope)));
759
1086
  if (signal.aborted) {
@@ -770,11 +1097,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
770
1097
  }, awaitExecutor(() => (admitted
771
1098
  ? admitted.execute(wrapGuestProgram(program), [provider])
772
1099
  : executor.execute(wrapGuestProgram(program), [provider])).then((outcome) => {
773
- executorLogs = sentSecrets.redact(outcome?.logs);
1100
+ executorLogs = sentSecrets.redact(outcome?.logs, "prose");
774
1101
  return sentSecrets.redact(outcome);
775
1102
  }, (err) => {
776
1103
  if (err !== null && typeof err === "object" && "logs" in err) {
777
- executorLogs = sentSecrets.redact(err.logs);
1104
+ executorLogs = sentSecrets.redact(err.logs, "prose");
778
1105
  }
779
1106
  throw sentSecrets.redact(err);
780
1107
  }), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
@@ -783,13 +1110,18 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
783
1110
  return programWrites.drain();
784
1111
  }))));
785
1112
  });
786
- const reported = { emitted, diagnostics, program };
1113
+ const reported = {
1114
+ emitted,
1115
+ diagnostics,
1116
+ program,
1117
+ guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, config.trust, logger, maxChars, programWrites.hasSucceeded),
1118
+ };
787
1119
  const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
788
1120
  // Releasing a QuickJS lease ends its child and rejects execute() with
789
1121
  // the retained log prefix. Give that report one timer turn, just as
790
1122
  // cancellation does in awaitExecutor; never wait on a Worker isolate.
791
1123
  budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
792
- return Effect.map(Effect.map(exited, (exit) => {
1124
+ return Effect.map(Effect.flatMap(exited, (exit) => {
793
1125
  // A synchronous fire-and-forget burst can also settle its executor in
794
1126
  // this turn. The host's terminal refusal always wins over that value.
795
1127
  if (budgetFailure instanceof HostCallBudgetExceeded) {
@@ -799,12 +1131,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
799
1131
  emitted,
800
1132
  diagnostics,
801
1133
  });
802
- return failed;
1134
+ return Effect.succeed(failed);
803
1135
  }
804
1136
  if (Exit.isFailure(exit)) {
805
- return failedRun(sentSecrets.redact(Cause.squash(exit.cause)), logger, reported);
1137
+ return Effect.succeed(failedRun(redactAgentOutput(sentSecrets, Cause.squash(exit.cause)), logger, reported));
806
1138
  }
807
- return finishedRun(sentSecrets.redact(exit.value), reported);
1139
+ return Effect.promise(() => finishedRun(redactAgentOutput(sentSecrets, exit.value), reported));
808
1140
  }), (unfinished) => {
809
1141
  // Every run response passes through write accounting.
810
1142
  const response = programWrites.finish(unfinished);
@@ -874,7 +1206,7 @@ function failedRun(err, logger, { emitted, diagnostics, program }) {
874
1206
  });
875
1207
  }
876
1208
  /** The response for an ExecuteResult: the program's error or its value. */
877
- function finishedRun(outcome, { emitted, diagnostics, program }) {
1209
+ async function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
878
1210
  if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
879
1211
  return failureResponse("Executor failed: expected an ExecuteResult object.", {
880
1212
  emitted,
@@ -925,7 +1257,7 @@ function finishedRun(outcome, { emitted, diagnostics, program }) {
925
1257
  // error path so captured logs survive instead of a raw SDK 500.
926
1258
  let result;
927
1259
  try {
928
- result = guardExecuteResultValue(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
1260
+ result = await guardResult(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
929
1261
  }
930
1262
  catch (err) {
931
1263
  const message = `Error: result is not JSON-serializable: ${msg(err)}`;
@@ -1007,15 +1339,15 @@ function connectorInventory(connectors) {
1007
1339
  const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, trust) => `One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. ${trust === "trusted" ? "Trusted pool: programs may read and write; the host approves the program as a write." : "Read-only pool: programs read; writes use call_destructive_tool."} Limits: ${hostLimits.maxHostCalls} host calls, ${maxWrites} writes, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
1008
1340
  ${connectorInventory(connectors)}
1009
1341
 
1010
- Use async () => { ... }, no arguments, with the connecta global. No portable ambient capabilities. API notation:
1342
+ Use async () => { ... }, no arguments. No portable ambient capabilities. API:
1011
1343
  \`\`\`ts
1012
1344
  declare const connecta: {
1013
1345
  ${GUEST_API_DECLARATION}
1014
1346
  };
1015
1347
  \`\`\`
1016
- Discovery: { tools }; check search catalogErrors/absence. JSON schemas default; compact is text. GuestResult is { data, format: "json" | "text" }; inspect format before fields. Pages: text, hasMore, nextOffset in UTF-8 bytes. Promise.allSettled keeps typed failures. Host-call budget_exceeded is terminal, even through catch/allSettled. Runs report hostCalls.
1348
+ Search: {tools}, catalogErrors. JSON schemas default; compact text. Calls: {data, format:"json"|"text"}; >1 MiB: paged handle. Oversized returns: resultId/nextAction. connecta.result pages; UTF-8 byte offsets. allSettled keeps typed errors; host budget_exceeded ends the run.
1017
1349
 
1018
- Reduce JSON. Never repeat writes for output. emit: text or base64 image/audio, safe without await, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares the result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
1350
+ Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
1019
1351
  // Module scope, like the other five meta-tool inputs: its JSON Schema is
1020
1352
  // derived once per process. Budgets and connectors vary by deployment and
1021
1353
  // view, so they live in the description, never here.