@zackbart/connecta 0.41.0 → 0.42.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 (148) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +2 -1
  7. package/dist/activity.js +5 -5
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-routes.js +31 -5
  10. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  11. package/dist/auth/downstream-oauth.js +3 -0
  12. package/dist/auth/static-oauth.js +26 -18
  13. package/dist/auth-elicitation.js +118 -61
  14. package/dist/authorization-handoff.js +36 -3
  15. package/dist/catalog-cache.js +292 -24
  16. package/dist/catalog-freshness.js +12 -0
  17. package/dist/catalog-service.js +373 -70
  18. package/dist/catalog.js +147 -58
  19. package/dist/config-defaults.d.ts +2 -1
  20. package/dist/config-defaults.js +4 -1
  21. package/dist/config.d.ts +43 -17
  22. package/dist/config.js +26 -10
  23. package/dist/connectors/remote-mcp/catalog.js +52 -28
  24. package/dist/connectors/remote-mcp/connection.js +62 -5
  25. package/dist/connectors/remote-mcp.js +10 -1
  26. package/dist/describe-config.d.ts +3 -2
  27. package/dist/discovery-health.js +26 -11
  28. package/dist/errors.d.ts +52 -3
  29. package/dist/errors.js +86 -3
  30. package/dist/execute.js +476 -200
  31. package/dist/executor-admission.js +2 -0
  32. package/dist/executor-contract.js +6 -0
  33. package/dist/executor-result.js +2 -2
  34. package/dist/executors/quickjs-child.js +13 -4
  35. package/dist/executors/quickjs-runtime.d.ts +2 -0
  36. package/dist/executors/quickjs-runtime.js +36 -19
  37. package/dist/executors/quickjs.js +23 -6
  38. package/dist/guest-functions.js +5 -4
  39. package/dist/guest-runtime.js +37 -1
  40. package/dist/guest-types.d.ts +18 -0
  41. package/dist/http/client.js +7 -2
  42. package/dist/http/errors.js +9 -4
  43. package/dist/http/fields.js +17 -5
  44. package/dist/http/hatch.js +9 -3
  45. package/dist/http/paging.js +1 -1
  46. package/dist/http/provider.js +2 -6
  47. package/dist/http/vendor.js +6 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +5 -0
  50. package/dist/invocation.js +98 -34
  51. package/dist/meta-output.js +24 -18
  52. package/dist/meta-tool-names.d.ts +1 -1
  53. package/dist/meta-tool-names.js +1 -1
  54. package/dist/meta-tools.js +259 -60
  55. package/dist/native-recovery.js +4 -1
  56. package/dist/native-tools.js +166 -51
  57. package/dist/node.js +3 -1
  58. package/dist/oauth-handoff.js +53 -43
  59. package/dist/operation-activity.js +2 -1
  60. package/dist/operation-vocabulary.js +6 -0
  61. package/dist/operator-record.d.ts +1 -1
  62. package/dist/operator-record.js +1 -0
  63. package/dist/operator-ui/generated.js +2 -2
  64. package/dist/program-result.js +2 -0
  65. package/dist/program-source.js +1 -1
  66. package/dist/program-writes.js +112 -19
  67. package/dist/provider.d.ts +0 -13
  68. package/dist/provider.js +0 -3
  69. package/dist/providers/_shared/google/kit.js +2 -2
  70. package/dist/providers/breeze/api.js +2 -2
  71. package/dist/providers/breeze/skill.generated.js +1 -1
  72. package/dist/providers/ccb/api.js +2 -2
  73. package/dist/providers/ccb/skill.generated.js +1 -1
  74. package/dist/providers/cloudflare/api.js +2 -2
  75. package/dist/providers/cloudflare/index.js +2 -0
  76. package/dist/providers/cloudflare/skill.generated.js +1 -1
  77. package/dist/providers/docs/skill.generated.js +1 -1
  78. package/dist/providers/drive/skill.generated.js +1 -1
  79. package/dist/providers/forms/index.js +1 -0
  80. package/dist/providers/forms/skill.generated.js +1 -1
  81. package/dist/providers/github/api.js +98 -6
  82. package/dist/providers/github/index.js +7 -2
  83. package/dist/providers/github/lib/auth.d.ts +3 -1
  84. package/dist/providers/github/lib/auth.js +43 -12
  85. package/dist/providers/github/lib/discovery.js +11 -1
  86. package/dist/providers/github/lib/operations.js +8 -0
  87. package/dist/providers/github/safety.js +39 -0
  88. package/dist/providers/github/skill.generated.js +1 -1
  89. package/dist/providers/github/tools.js +12 -5
  90. package/dist/providers/gmail/index.js +1 -0
  91. package/dist/providers/gmail/skill.generated.js +1 -1
  92. package/dist/providers/granola/skill.generated.js +1 -1
  93. package/dist/providers/overflow/index.js +2 -0
  94. package/dist/providers/overflow/skill.generated.js +1 -1
  95. package/dist/providers/planning-center/index.js +11 -5
  96. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  97. package/dist/providers/planning-center/skill.generated.js +1 -1
  98. package/dist/providers/quickbooks/index.js +2 -5
  99. package/dist/providers/quickbooks/skill.generated.js +1 -1
  100. package/dist/providers/sheets/index.js +1 -0
  101. package/dist/providers/sheets/skill.generated.js +1 -1
  102. package/dist/providers/slides/index.js +2 -2
  103. package/dist/providers/slides/skill.generated.js +1 -1
  104. package/dist/providers/stripe/skill.generated.js +1 -1
  105. package/dist/providers/tithely/index.js +1 -0
  106. package/dist/providers/tithely/skill.generated.js +1 -1
  107. package/dist/providers/vercel/api.js +2 -2
  108. package/dist/providers/vercel/skill.generated.js +1 -1
  109. package/dist/registry.d.ts +36 -4
  110. package/dist/registry.js +127 -27
  111. package/dist/result-preview.js +210 -0
  112. package/dist/result-seal.js +168 -0
  113. package/dist/route-names.js +13 -4
  114. package/dist/routes/connect.js +21 -13
  115. package/dist/routes/mcp.js +55 -26
  116. package/dist/routes/shared.d.ts +6 -0
  117. package/dist/runtime/call.js +15 -10
  118. package/dist/runtime/request-scope.js +36 -1
  119. package/dist/runtime/run.js +4 -1
  120. package/dist/sent-secrets.d.ts +20 -0
  121. package/dist/sent-secrets.js +49 -1
  122. package/dist/skills.js +100 -33
  123. package/dist/storage/keys.js +9 -6
  124. package/dist/storage/sql-native.js +1 -1
  125. package/dist/tool-safety.d.ts +33 -0
  126. package/dist/tool-safety.js +51 -15
  127. package/dist/types.d.ts +31 -0
  128. package/dist/usage-guide.js +14 -13
  129. package/dist/validate.js +29 -10
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/dist/worker.js +28 -8
  133. package/documentation/architecture.md +104 -42
  134. package/documentation/auth.md +46 -19
  135. package/documentation/code-mode.md +378 -116
  136. package/documentation/deploying.md +10 -9
  137. package/documentation/hosted-mcp-migration.md +9 -3
  138. package/documentation/integrating.md +27 -5
  139. package/documentation/meta-tools.md +655 -153
  140. package/documentation/operating.md +5 -3
  141. package/documentation/providers.md +18 -6
  142. package/examples/worker/README.md +3 -3
  143. package/guides/how-it-works.md +2 -1
  144. package/guides/safety-and-trust.md +1 -3
  145. package/package.json +1 -1
  146. package/templates/node/AGENTS.md +3 -3
  147. package/templates/node/README.md +1 -1
  148. package/templates/node/package.json +1 -1
package/dist/execute.js CHANGED
@@ -2,6 +2,7 @@ import { operationActivity } from "./operation-activity.js";
2
2
  import { SkillLookupError } from "./skills.js";
3
3
  import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
4
4
  import { META_ROUTES, routeNames } from "./agent-routes.js";
5
+ import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
5
6
  import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
6
7
  import { nativeRecovery } from "./native-recovery.js";
7
8
  import { GUEST_API_DECLARATION, GUEST_FUNCTIONS, UNCOUNTED_GUEST_FUNCTIONS, } from "./guest-functions.js";
@@ -11,29 +12,38 @@ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
11
12
  import * as z from "zod";
12
13
  import { advertisedSchema } from "./advertised-schema.js";
13
14
  import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
14
- import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
15
- import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
15
+ import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, releaseStashedResult, } from "./meta-tools.js";
16
+ import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_READ_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
16
17
  import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
17
18
  import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
18
- import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
19
+ import { boundedEchoText, clipSerialized, ConnectorCallError, msg } from "./errors.js";
19
20
  import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
20
21
  import { InvocationFailure, InvocationService, timed } from "./invocation.js";
21
22
  import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
22
23
  import { EXECUTE_OUTPUT } from "./meta-output.js";
23
24
  import { normalizeProgramSource } from "./program-source.js";
24
25
  import { readResource } from "./resource-read.js";
26
+ import { failureRecord, logFailure } from "./operator-record.js";
27
+ import { HeldResults, sealHeldResults } from "./result-seal.js";
25
28
  import { redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
26
29
  import { normalizeTimeoutMs } from "./timeout.js";
27
30
  import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
28
31
  import { closeScope } from "./runtime/connector-scope.js";
29
32
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
30
33
  import { executorProgram } from "./runtime/executor.js";
31
- import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
34
+ import { connectorGuide, connectorGuideRequired, addsInformation, connectorInventoryTitle, inventoryNote, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
32
35
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
33
36
  const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
34
37
  const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
35
38
  /** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
36
39
  const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
40
+ /** All of one run's stash reclaim, in milliseconds of waiting on storage. */
41
+ const RECLAIM_BUDGET_MS = 1_000;
42
+ /**
43
+ * Reclaims per run. Each freed slot lets another read stash and page again, so unbounded
44
+ * reclaim keeps a program spending storage queries that a full stash would have ended.
45
+ */
46
+ const RECLAIM_MAX_PER_RUN = 3;
37
47
  /**
38
48
  * The outer ceiling on one execution. It sits above both executors' own
39
49
  * deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
@@ -41,6 +51,41 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
41
51
  * never settles at all.
42
52
  */
43
53
  const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
54
+ /**
55
+ * How long before a deadline a program's reads stop: the time the guest gets
56
+ * to receive their `timeout` and return what it has.
57
+ */
58
+ const READ_DEADLINE_MARGIN_MS = 1_000;
59
+ const readMarginMs = (deadlineMs) => Math.min(READ_DEADLINE_MARGIN_MS, Math.floor(deadlineMs / 10));
60
+ /**
61
+ * When a run's reads stop: before the executor's deadline, measured from now,
62
+ * and before the response ceiling, which queueing may already have spent.
63
+ */
64
+ function readDeadlineAt(executor, ceiling) {
65
+ const beforeCeiling = ceiling.at - readMarginMs(ceiling.ms);
66
+ const { deadlineMs } = executor;
67
+ if (deadlineMs === undefined)
68
+ return beforeCeiling;
69
+ return Math.min(beforeCeiling, Date.now() + deadlineMs - readMarginMs(deadlineMs));
70
+ }
71
+ /** Past the ceiling, room to store what a run leaves behind. Default 57 s against a 60 s client. */
72
+ const RESPONSE_STORAGE_MARGIN_MS = 2_000;
73
+ function responseCeiling(startedAt, ms) {
74
+ const at = startedAt + ms;
75
+ return { ms, at, startedAt, answerBy: at + Math.min(RESPONSE_STORAGE_MARGIN_MS, Math.floor(ms / 10)) };
76
+ }
77
+ /** Refuse a run with no time left to start. Nothing has run, so running it again is safe. */
78
+ const startable = (ceiling) => Effect.suspend(() => {
79
+ const elapsedMs = Date.now() - ceiling.startedAt;
80
+ return elapsedMs < ceiling.ms
81
+ ? Effect.void
82
+ : Effect.fail(new InvocationFailure({
83
+ code: "timeout",
84
+ message: `Operation "execute_code" used its ${ceiling.ms}ms response ceiling before it started, so it did not run and sent nothing. Run it again.`,
85
+ retryable: true,
86
+ details: { operation: "execute_code", stage: "response ceiling", elapsedMs, deadlineMs: ceiling.ms },
87
+ }));
88
+ });
44
89
  /**
45
90
  * Room to name every connector of a typical deployment. Past it, complete
46
91
  * entries plus an exact omission count stay inside this byte cap.
@@ -351,31 +396,16 @@ function guestFailure(code, message, retryable = false) {
351
396
  }
352
397
  /**
353
398
  * E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
399
+ * The guest keeps this text, and may return it after a later call registers a credential it
400
+ * holds, so the message is cut between tokens.
354
401
  * Keep optional recovery whole: clipping an address or echoed args could turn
355
402
  * recovery into a different call. Oversized metadata is omitted instead.
356
403
  */
357
404
  function boundedGuestFailure(failure) {
358
- const boundText = (text, maxChars) => {
359
- if (JSON.stringify(text).length <= maxChars)
360
- return text;
361
- let low = 0;
362
- let high = Math.min(text.length, maxChars);
363
- while (low < high) {
364
- const mid = Math.ceil((low + high) / 2);
365
- if (JSON.stringify(`${text.slice(0, mid)}…`).length <= maxChars)
366
- low = mid;
367
- else
368
- high = mid - 1;
369
- }
370
- // Do not manufacture a lone surrogate when clipping a Unicode message.
371
- if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1]))
372
- low--;
373
- return `${text.slice(0, low)}…`;
374
- };
375
405
  let details = {
376
406
  ...failure.details,
377
- code: boundText(failure.details.code, 128),
378
- message: boundText(failure.details.message, 2_000),
407
+ code: clipSerialized(failure.details.code, 128),
408
+ message: clipSerialized(failure.details.message, 2_000),
379
409
  };
380
410
  // Keep typed metadata below the executor bridge bound.
381
411
  if (JSON.stringify(details).length > 3_700) {
@@ -401,7 +431,7 @@ class HostCallBudgetExceeded extends InvocationFailure {
401
431
  constructor(hostCalls, maxHostCalls) {
402
432
  super({
403
433
  code: "budget_exceeded",
404
- message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
434
+ message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum); the run returned nothing. Split the rest across programs of at most ${maxHostCalls} calls each; do not resend any write it sent.`,
405
435
  retryable: false,
406
436
  });
407
437
  this.hostCalls = hostCalls;
@@ -431,6 +461,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
431
461
  readOnly: limits.readOnly,
432
462
  // A program that just missed an address cannot call search_tools.
433
463
  searchRoute: "connecta.search",
464
+ markUncallableWrites: limits.readProgram,
434
465
  concurrency: limits.discoveryConcurrency,
435
466
  probeTimeoutMs: limits.probeTimeoutMs,
436
467
  defer: limits.defer,
@@ -440,6 +471,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
440
471
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
441
472
  const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
442
473
  const { signal, diagnostics, programWrites } = limits;
474
+ /** The part of `requestedMs` left before reads stop; never below one tick. */
475
+ const readBudget = (requestedMs) => Math.min(requestedMs, Math.max(1, (limits.readDeadlineAt ?? Infinity) - Date.now()));
443
476
  const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
444
477
  let budgetFailure;
445
478
  let emitAttempts = 0;
@@ -460,33 +493,70 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
460
493
  const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
461
494
  let writes = 0;
462
495
  // These are transfer cursors only. All result bytes live in the existing stash.
496
+ // `reclaim` marks a read's stash only this transfer can name, because the guest never saw
497
+ // its handle. A guest-supplied id stays readable until it expires; a write's stash expires too.
498
+ const readBytes = limits.readBytes ?? PROGRAM_RESULT_READ_BYTES;
463
499
  const transfers = new Map();
464
500
  const nativeResults = new Map();
465
- const transportResult = (value) => {
501
+ const transportResult = (value, write) => {
466
502
  if (value.format === "paged") {
467
503
  const handle = value;
468
504
  if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
469
505
  const transfer = `transfer:${crypto.randomUUID()}`;
470
- transfers.set(transfer, { id: handle.resultId, offset: 0 });
471
- return { transfer, handle };
506
+ transfers.set(transfer, {
507
+ id: handle.resultId,
508
+ offset: 0,
509
+ ...(write ? {} : { reclaim: { totalBytes: handle.totalBytes } }),
510
+ });
511
+ return { transfer, handle, ...(write ? {} : { reclaim: true }) };
472
512
  }
473
513
  }
474
514
  return value;
475
515
  };
516
+ // Slow storage may cost a run its reclaim, never its answer: all of a run's reclaim shares
517
+ // this much time, and once it's spent the rest of the run's entries just wait to expire.
518
+ let reclaimLeftMs = RECLAIM_BUDGET_MS;
519
+ let reclaimsLeft = RECLAIM_MAX_PER_RUN;
520
+ const reclaim = (id, totalBytes) => Effect.suspend(() => {
521
+ if (reclaimLeftMs <= 0 || reclaimsLeft <= 0)
522
+ return Effect.void;
523
+ reclaimsLeft--;
524
+ const started = Date.now();
525
+ return releaseStashedResult(registry, limits.heldResults, id, totalBytes).pipe(Effect.timeoutOrElse({ duration: Duration.millis(reclaimLeftMs), orElse: () => Effect.void }), Effect.ignore, Effect.ensuring(Effect.sync(() => {
526
+ reclaimLeftMs -= Date.now() - started;
527
+ })));
528
+ });
529
+ const dispatchDeadlineAt = limits.readDeadlineAt ?? limits.programDeadlineAt;
476
530
  /** Budget and account for writes a writable endpoint admits. */
477
- const invocationContext = (sending, timeoutMs) => ({
478
- source: "execute_code",
531
+ const invocationContext = (sending, timeoutMs, writeTimeoutMs) => ({
532
+ source: limits.readProgram ? READ_PROGRAM_SOURCE : "execute_code",
479
533
  sentSecrets,
480
534
  timeoutMs,
535
+ writeTimeoutMs,
481
536
  ...(limits.routes ? { routes: limits.routes } : {}),
482
537
  ...(signal !== undefined ? { requestSignal: signal } : {}),
483
538
  ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
539
+ ...(dispatchDeadlineAt !== undefined
540
+ ? {
541
+ // Past the gate's time, a call's answer could not reach the program either.
542
+ dispatchDeadline: {
543
+ at: dispatchDeadlineAt,
544
+ refusal: (address) => ({
545
+ code: "timeout",
546
+ message: `Too little of this run's deadline remains for ${address}, so it was not sent. Return what you have and send it in a new program.`,
547
+ retryable: true,
548
+ details: { operation: address, stage: "dispatch gate" },
549
+ }),
550
+ },
551
+ }
552
+ : {}),
484
553
  unwrapResult: true,
485
554
  readOnly: limits.readOnly,
486
555
  processResult: authMethod((value, resolved, secrets, format, raw) => Effect.gen(function* () {
487
556
  // Dispatch succeeded before presentation began. Cancellation of paging
488
557
  // cannot turn this completed write into an unknown or failed write.
489
558
  sending.settle?.("ok");
559
+ const write = resolved.definition.classification !== "read";
490
560
  const page = (data, valueFormat, wireBytes) => authCall(pageProgramResult, [
491
561
  registry,
492
562
  baseUrl,
@@ -495,7 +565,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
495
565
  valueFormat,
496
566
  secrets,
497
567
  requestScope,
568
+ { max: maxHostCalls, left: maxHostCalls - hostCalls.attempted },
498
569
  wireBytes,
570
+ limits.followUpRoutes,
571
+ limits.heldResults,
572
+ readBytes,
499
573
  ]);
500
574
  const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
501
575
  ? raw
@@ -516,7 +590,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
516
590
  const content = envelope?.content;
517
591
  if (!Array.isArray(content) ||
518
592
  !content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
519
- return transportResult(yield* page(value, format));
593
+ return transportResult(yield* page(value, format), write);
520
594
  }
521
595
  const unavailablePresentation = () => ({
522
596
  format: "paged",
@@ -542,7 +616,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
542
616
  catch (error) {
543
617
  if (!(error instanceof NativePresentationLimit))
544
618
  throw error;
545
- const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format));
619
+ const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format), write);
546
620
  const notice = unavailablePresentation();
547
621
  return result ? { value: result, native: { value: notice } } : notice;
548
622
  }
@@ -556,7 +630,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
556
630
  });
557
631
  let valueResult;
558
632
  if (native.format === "paged") {
559
- valueResult = result ? transportResult(result) : undefined;
633
+ valueResult = result ? transportResult(result, write) : undefined;
560
634
  if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
561
635
  return valueResult ? { value: valueResult, native: { value: native } } : native;
562
636
  }
@@ -566,14 +640,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
566
640
  // absent or connector-owned blocks.
567
641
  if (!retained) {
568
642
  const notice = native.format === "paged" ? native : unavailablePresentation();
569
- const valueResult = result ? transportResult(result) : undefined;
643
+ const valueResult = result ? transportResult(result, write) : undefined;
570
644
  return valueResult ? { value: valueResult, native: { value: notice } } : notice;
571
645
  }
572
646
  const ref = crypto.randomUUID();
573
647
  nativeResults.set(ref, retained);
574
648
  return {
575
649
  ...(valueResult ? { value: valueResult } : {}),
576
- native: { ref, value: transportResult(native), dataKey },
650
+ native: { ref, value: transportResult(native, write), dataKey },
577
651
  };
578
652
  })),
579
653
  beforeWrite: (target) => Effect.sync(() => {
@@ -588,13 +662,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
588
662
  unrecorded: true,
589
663
  };
590
664
  }
665
+ // A write is exactly what its program asked for, or it stays here. Reads
666
+ // keep the filtered value: sending one changes nothing it could not repeat.
667
+ if (sending.rewritten) {
668
+ return {
669
+ kind: "refuse",
670
+ error: {
671
+ code: "invalid_args",
672
+ message: `${target.connector.id}.${target.toolName} was not sent: its arguments hold a credential this request sent, and a write is never sent altered. If the value is genuine, send it as its own direct call.`,
673
+ retryable: false,
674
+ },
675
+ };
676
+ }
677
+ // Its answer could not reach the program, so refuse now while the outcome
678
+ // is known. Earlier writes go out and drain past the deadline (W9).
679
+ if (limits.readDeadlineAt !== undefined && Date.now() >= limits.readDeadlineAt) {
680
+ return {
681
+ kind: "refuse",
682
+ error: {
683
+ code: "timeout",
684
+ message: `Too little of this run's deadline remains for ${target.connector.id}.${target.toolName}, so it was not sent. Return what you have and send it in a new program.`,
685
+ retryable: true,
686
+ details: { operation: `${target.connector.id}.${target.toolName}`, stage: "write gate" },
687
+ },
688
+ };
689
+ }
591
690
  const budgeted = target.definition.classification !== "read";
592
691
  if (budgeted && writes >= maxWrites) {
593
692
  return {
594
693
  kind: "refuse",
595
694
  error: {
596
695
  code: "budget_exceeded",
597
- message: `execute_code write budget exceeded (${maxWrites} writes maximum, execute.maxWrites); ${target.connector.id}.${target.toolName} was not sent`,
696
+ message: `execute_code write budget exceeded (${maxWrites} writes per run); ${target.connector.id}.${target.toolName} was not sent. Check the run's writes count, then send only the rest in a new program; do not resend those that succeeded.`,
598
697
  retryable: false,
599
698
  },
600
699
  };
@@ -638,12 +737,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
638
737
  const requestedMs = normalizeTimeoutMs(timeout) ??
639
738
  normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
640
739
  hostCallTimeoutMs;
641
- const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
740
+ const timeoutMs = readBudget(requestedMs);
741
+ const writeTimeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
642
742
  // A program write settles here — unknown if the call never returned an
643
743
  // outcome. Credentials are filtered from arguments; submitted private values are not.
644
- const sending = {};
744
+ const filtered = sentSecrets.filterCredentials(args ?? {});
745
+ const sending = { rewritten: filtered.changed };
645
746
  const outcome = yield* invocation
646
- .pipeline(address, sentSecrets.redactCredentials(args ?? {}), invocationContext(sending, timeoutMs))
747
+ .pipeline(address, filtered.value, invocationContext(sending, timeoutMs, writeTimeoutMs))
647
748
  .pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
648
749
  if (outcome.resolved) {
649
750
  const resolved = outcome.resolved;
@@ -679,19 +780,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
679
780
  const discovery = (operation, read) => Effect.suspend(() => {
680
781
  const started = Date.now();
681
782
  const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
682
- const reading = Effect.tryPromise({
683
- try: read,
684
- catch: (err) => (err instanceof DiscoveryPolicyError ? guestFailure(err.code, err.message) : err),
685
- });
686
- return (!hostAccessSignal
687
- ? reading
688
- : hostAccessSignal.aborted
689
- ? Effect.fail(cancelled())
690
- : 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))));
783
+ // In the race itself, not behind a Promise: losing interrupts the catalog
784
+ // work and aborts its listing before the program hears the outcome (L3).
785
+ // The catalog hears the run's end as well, so its failure may arrive first.
786
+ const reading = Effect.suspend(read).pipe(Effect.catchDefect((err) => Effect.fail(err)), Effect.mapError((err) => hostAccessSignal?.aborted
787
+ ? cancelled()
788
+ : err instanceof DiscoveryPolicyError
789
+ ? guestFailure(err.code, err.message)
790
+ : err));
791
+ const contenders = [reading];
792
+ // A catalog probe may outlast the run; end it as a catchable timeout first.
793
+ if (limits.readDeadlineAt !== undefined)
794
+ contenders.push(Effect.suspend(() => {
795
+ const deadlineMs = readBudget(Infinity);
796
+ return Effect.sleep(Duration.millis(deadlineMs)).pipe(Effect.andThen(Effect.suspend(() => {
797
+ const elapsedMs = Date.now() - started;
798
+ const name = `connecta.${operation}`;
799
+ return Effect.fail(new InvocationFailure({
800
+ code: "timeout",
801
+ message: `Operation "${name}" timed out during catalog after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`,
802
+ retryable: true,
803
+ details: { operation: name, stage: "catalog", elapsedMs, deadlineMs },
804
+ }));
805
+ })));
806
+ }));
807
+ if (hostAccessSignal)
808
+ contenders.push(fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled)));
809
+ return (hostAccessSignal?.aborted ? Effect.fail(cancelled()) : Effect.raceAllFirst(contenders)).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
691
810
  });
692
811
  const meta = createMetaTools(registry, baseUrl, {
693
812
  readOnly: limits.readOnly,
694
813
  requestScope,
814
+ heldResults: limits.heldResults,
695
815
  onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
696
816
  });
697
817
  const catalogDependency = (connector, tool) => {
@@ -709,63 +829,93 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
709
829
  catalogDependency(resolved.connector.id, resolved.toolName);
710
830
  };
711
831
  const operations = {
712
- // Uncounted: a failed program read a result field off an unawaited host call.
832
+ // Uncounted: a program read a result field off an unawaited host call.
713
833
  unawaited: () => {
714
834
  if (limits.unawaited)
715
835
  limits.unawaited.read = true;
716
836
  },
717
837
  call,
718
- result: (id, options, signal, onStage) => Effect.tryPromise({
719
- try: async () => {
720
- const transfer = typeof id === "string" ? transfers.get(id) : undefined;
721
- const transferId = id;
722
- if (transfer) {
723
- id = transfer.id;
724
- options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
725
- }
726
- else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
727
- id = id.resultId;
728
- }
729
- if (typeof id !== "string" ||
730
- (!options && options !== undefined) ||
731
- (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
732
- throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? })."));
733
- }
734
- const args = { ...(options ?? {}) };
735
- if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
736
- (args.page !== undefined &&
737
- (typeof args.page !== "number" ||
738
- !Number.isSafeInteger(args.page) ||
739
- args.page < 0 ||
740
- args.offset !== undefined)) ||
741
- (args.offset !== undefined &&
742
- (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
743
- (args.maxBytes !== undefined &&
744
- (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
745
- throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive."));
746
- }
747
- // A page must fit every shipped bridge even when the connector cap is raised.
748
- args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
749
- const page = await meta.readResult({ id, ...args }, {
750
- signal: signal,
751
- onStage: onStage,
752
- });
753
- if (page.isError)
754
- throw new InvocationFailure((page.structuredContent?.error ?? {
755
- code: "not_found",
756
- message: page.content[0]?.text ?? "Result unavailable.",
757
- retryable: false,
758
- }));
759
- if (transfer) {
760
- if (page.structuredContent?.hasMore)
761
- transfer.offset = page.structuredContent.nextOffset;
762
- else
763
- transfers.delete(transferId);
764
- }
765
- return page.structuredContent;
766
- },
767
- catch: (err) => err,
768
- }),
838
+ result: (id, options, signal, onStage) => {
839
+ const finished = typeof id === "string" ? transfers.get(id) : undefined;
840
+ if (finished?.finished) {
841
+ // The prelude has the whole value, so a read gives its bytes back.
842
+ transfers.delete(id);
843
+ return finished.reclaim
844
+ ? reclaim(finished.id, finished.reclaim.totalBytes).pipe(Effect.as({}))
845
+ : Effect.succeed({});
846
+ }
847
+ return Effect.tryPromise({
848
+ try: async () => {
849
+ const transfer = typeof id === "string" ? transfers.get(id) : undefined;
850
+ const transferId = id;
851
+ if (transfer) {
852
+ id = transfer.id;
853
+ options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
854
+ }
855
+ else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
856
+ id = id.resultId;
857
+ }
858
+ if (typeof id !== "string" ||
859
+ (!options && options !== undefined) ||
860
+ (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
861
+ throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? })."));
862
+ }
863
+ const args = { ...(options ?? {}) };
864
+ const whole = args.all === true;
865
+ if (args.all === false)
866
+ delete args.all;
867
+ if ((whole && Object.keys(args).length > 1) || (args.all !== undefined && !whole)) {
868
+ throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { all: true }) with no other options."));
869
+ }
870
+ delete args.all;
871
+ if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
872
+ (args.page !== undefined &&
873
+ (typeof args.page !== "number" ||
874
+ !Number.isSafeInteger(args.page) ||
875
+ args.page < 0 ||
876
+ args.offset !== undefined)) ||
877
+ (args.offset !== undefined &&
878
+ (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
879
+ (args.maxBytes !== undefined &&
880
+ (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
881
+ throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive."));
882
+ }
883
+ // A page must fit every shipped bridge even when the connector cap is raised.
884
+ args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
885
+ const page = await meta.readResult({ id, ...args }, {
886
+ signal: signal,
887
+ onStage: onStage,
888
+ });
889
+ if (page.isError)
890
+ throw new InvocationFailure((page.structuredContent?.error ?? {
891
+ code: "not_found",
892
+ message: page.content[0]?.text ?? "Result unavailable.",
893
+ retryable: false,
894
+ }));
895
+ if (transfer) {
896
+ if (page.structuredContent?.hasMore)
897
+ transfer.offset = page.structuredContent.nextOffset;
898
+ else if (transfer.reclaim)
899
+ transfer.finished = true;
900
+ else
901
+ transfers.delete(transferId);
902
+ }
903
+ if (whole) {
904
+ const totalBytes = page.structuredContent?.totalBytes;
905
+ if (totalBytes > readBytes) {
906
+ throw guestFailure("result_too_large", `This result is ${totalBytes} bytes and connecta.result(id, { all: true }) reads at most ${readBytes}. Page it with { offset } and keep only what you need from each page.`);
907
+ }
908
+ if (!page.structuredContent?.hasMore)
909
+ return page.structuredContent;
910
+ const started = `transfer:${crypto.randomUUID()}`;
911
+ transfers.set(started, { id, offset: page.structuredContent.nextOffset });
912
+ return { ...page.structuredContent, transfer: started };
913
+ }
914
+ return page.structuredContent;
915
+ },
916
+ catch: (err) => err,
917
+ });
918
+ },
769
919
  skill: (name, _options, utilitySignal) => Effect.tryPromise({
770
920
  try: async () => {
771
921
  // Accept the top-level skills tool's { name } shape too: models copy it into programs.
@@ -799,7 +949,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
799
949
  baseUrl,
800
950
  requestScope,
801
951
  sentSecrets,
802
- timeoutMs: hostCallTimeoutMs,
952
+ timeoutMs: readBudget(hostCallTimeoutMs),
803
953
  onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
804
954
  onConnectorTime: (elapsed) => {
805
955
  connectorMs += elapsed;
@@ -839,34 +989,40 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
839
989
  },
840
990
  catch: (err) => err,
841
991
  }),
842
- search: (raw) => discovery("search", async () => {
992
+ search: (raw) => discovery("search", () => {
843
993
  const args = (raw ?? {});
844
- const result = flatSearchResult(await catalog.search({
994
+ return catalog
995
+ .searchEffect({
845
996
  ...args,
846
997
  includeSchemas: args.includeSchemas ?? "typescript",
847
998
  includeSchemaKeys: args.includeSchemaKeys !== false,
848
- }, catalogDependency));
849
- boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
850
- return result;
999
+ }, catalogDependency)
1000
+ .pipe(Effect.map((page) => {
1001
+ const result = flatSearchResult(page);
1002
+ boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
1003
+ return result;
1004
+ }));
851
1005
  }),
852
- describe: (raw) => discovery("describe", async () => {
1006
+ describe: (raw) => discovery("describe", () => {
853
1007
  const args = (raw ?? {});
854
- const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
855
- boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
856
- for (const tool of result.tools) {
857
- if (!tool.error)
858
- catalogAddressDependency(tool.address);
859
- else {
860
- const resolved = registry.resolveAddress(tool.address);
861
- if (resolved)
862
- catalogDependency(resolved.connector.id);
863
- for (const connector of tool.errorDetails?.configuredConnectors ?? [])
864
- catalogDependency(connector);
865
- for (const suggestion of tool.errorDetails?.suggestions ?? [])
866
- catalogAddressDependency(suggestion);
1008
+ return catalog.describeEffect({ ...args, format: args.format ?? "json" }).pipe(Effect.map((tools) => {
1009
+ const result = { tools };
1010
+ boundedDiscoveryText(result, 'Split the address list, omit fullDescriptions, or use format: "compact".');
1011
+ for (const tool of result.tools) {
1012
+ if (!tool.error)
1013
+ catalogAddressDependency(tool.address);
1014
+ else {
1015
+ const resolved = registry.resolveAddress(tool.address);
1016
+ if (resolved)
1017
+ catalogDependency(resolved.connector.id);
1018
+ for (const connector of tool.errorDetails?.configuredConnectors ?? [])
1019
+ catalogDependency(connector);
1020
+ for (const suggestion of tool.errorDetails?.suggestions ?? [])
1021
+ catalogAddressDependency(suggestion);
1022
+ }
867
1023
  }
868
- }
869
- return result;
1024
+ return result;
1025
+ }));
870
1026
  }),
871
1027
  };
872
1028
  // Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
@@ -898,7 +1054,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
898
1054
  return [
899
1055
  name,
900
1056
  (...args) => {
901
- if (budgetFailure)
1057
+ // Once sealing begins the run's secret set is final, so nothing new may start.
1058
+ if (budgetFailure || limits.heldResults?.accepts() === false)
902
1059
  return stopped;
903
1060
  // Uncounted: one per run; any further call through the bridge ends the run.
904
1061
  if (budget === "once") {
@@ -922,26 +1079,27 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
922
1079
  if (counted)
923
1080
  hostCalls.admitted++;
924
1081
  const started = Date.now();
1082
+ const utilityDeadlineMs = readBudget(hostCallTimeoutMs);
925
1083
  let stage = deadline ?? "";
926
1084
  const invoke = Effect.suspend(() => operation(...args));
927
1085
  const guarded = deadline
928
1086
  ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
929
1087
  stage = current;
930
1088
  })), {
931
- timeoutMs: hostCallTimeoutMs,
1089
+ timeoutMs: utilityDeadlineMs,
932
1090
  ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
933
1091
  timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
934
1092
  })
935
1093
  : invoke;
936
1094
  const operationEffect = activity?.recordOperation && ["search", "describe", "result", "skill"].includes(name)
937
- ? operationActivity(activity, registry, name, guarded)
1095
+ ? operationActivity(activity, registry, name, guarded, undefined, limits.readProgram ? READ_PROGRAM_SOURCE : undefined)
938
1096
  : guarded;
939
- const settled = runEdge(operationEffect.pipe(Effect.catch((error) => {
1097
+ const running = runEdge(operationEffect.pipe(Effect.catch((error) => {
940
1098
  if (deadline && hostAccessSignal?.aborted)
941
1099
  return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
942
1100
  if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
943
1101
  const elapsedMs = Date.now() - started;
944
- const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
1102
+ const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${utilityDeadlineMs}ms).`;
945
1103
  return typedFailure(new InvocationFailure({
946
1104
  code: "timeout",
947
1105
  message,
@@ -950,12 +1108,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
950
1108
  operation: `connecta.${name}`,
951
1109
  stage,
952
1110
  elapsedMs,
953
- deadlineMs: hostCallTimeoutMs,
1111
+ deadlineMs: utilityDeadlineMs,
954
1112
  },
955
1113
  }));
956
1114
  }
957
1115
  return typedFailure(error);
958
- }))).then((value) => {
1116
+ })));
1117
+ limits.heldResults?.track(running);
1118
+ const settled = running.then((value) => {
959
1119
  if (counted)
960
1120
  hostCalls.succeeded++;
961
1121
  return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
@@ -1002,7 +1162,6 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
1002
1162
  * waiting for one.
1003
1163
  */
1004
1164
  function awaitExecutor(start, signal, options = {}) {
1005
- const started = Date.now();
1006
1165
  const contenders = [
1007
1166
  typeof start !== "function"
1008
1167
  ? start
@@ -1029,21 +1188,34 @@ function awaitExecutor(start, signal, options = {}) {
1029
1188
  contenders.push(options.terminal);
1030
1189
  const { watchdog } = options;
1031
1190
  if (watchdog) {
1032
- contenders.push(Effect.sleep(Duration.millis(watchdog.ms)).pipe(Effect.andThen(Effect.suspend(() => {
1191
+ contenders.push(untilCeiling(watchdog.ceiling).pipe(Effect.andThen(Effect.suspend(() => {
1192
+ const { ms: deadlineMs, startedAt } = watchdog.ceiling;
1193
+ const elapsedMs = Date.now() - startedAt;
1194
+ // Time ran out before the executor's own deadline could: queueing
1195
+ // spent it, or that deadline is set past the ceiling. Nothing is wedged.
1196
+ if (watchdog.executorDeadlineAt !== undefined && Date.now() < watchdog.executorDeadlineAt) {
1197
+ return Effect.fail(new InvocationFailure({
1198
+ code: "timeout",
1199
+ message: `Operation "execute_code" timed out during response ceiling after ${elapsedMs}ms (effective deadline ${deadlineMs}ms). Return sooner, or do less in one program.`,
1200
+ retryable: true,
1201
+ details: { operation: "execute_code", stage: "response ceiling", elapsedMs, deadlineMs },
1202
+ }));
1203
+ }
1033
1204
  watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
1034
- watchdogMs: watchdog.ms,
1205
+ watchdogMs: deadlineMs,
1035
1206
  });
1036
- const elapsedMs = Date.now() - started;
1037
1207
  return Effect.fail(new InvocationFailure({
1038
1208
  code: "timeout",
1039
- message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${watchdog.ms}ms); sandbox unresponsive.`,
1209
+ message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${deadlineMs}ms); sandbox unresponsive.`,
1040
1210
  retryable: false,
1041
- details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs: watchdog.ms },
1211
+ details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs },
1042
1212
  }));
1043
1213
  }))));
1044
1214
  }
1045
1215
  return Effect.raceAllFirst(contenders);
1046
1216
  }
1217
+ /** Waits until the ceiling; already past it, one timer turn. */
1218
+ const untilCeiling = (ceiling) => Effect.suspend(() => Effect.sleep(Duration.millis(Math.max(1, ceiling.at - Date.now()))));
1047
1219
  /**
1048
1220
  * The run's own signal, aborted however the run ends. It follows the
1049
1221
  * caller's, and aborting it on the way out is what releases outstanding host
@@ -1072,9 +1244,11 @@ function runSignal(caller) {
1072
1244
  * executor, so this request's providers and program run here, never in the
1073
1245
  * releasing request, which workerd refuses.
1074
1246
  */
1075
- function leased(executor, signal) {
1247
+ function leased(executor, signal, ceiling) {
1076
1248
  return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
1077
1249
  late: (lease) => lease.release(),
1250
+ // A queue configured to outwait the client still answers it.
1251
+ terminal: untilCeiling(ceiling).pipe(Effect.andThen(Effect.suspend(() => Effect.fail(new ExecutorAdmissionError("executor_overloaded", `Executor admission timed out after ${Date.now() - ceiling.startedAt}ms.`))))),
1078
1252
  }), (lease) => Effect.sync(() => lease.release()));
1079
1253
  }
1080
1254
  /** The execute_code handler. Exported for direct testing. */
@@ -1085,12 +1259,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1085
1259
  };
1086
1260
  const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
1087
1261
  const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
1088
- const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites) => Effect.suspend(() => {
1262
+ const readBytes = resolveBudget(executor.maxReadBytes, PROGRAM_RESULT_READ_BYTES);
1263
+ const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling) => Effect.suspend(() => {
1089
1264
  const sentSecrets = sentSecretsForRequest(requestScope);
1265
+ const heldResults = new HeldResults(sentSecrets, requestOwner(requestScope).credentialReads);
1090
1266
  const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
1091
1267
  const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
1092
1268
  const resultDependencies = [];
1093
- const unawaited = { read: false };
1094
1269
  const dispatchController = new AbortController();
1095
1270
  const terminal = Deferred.makeUnsafe();
1096
1271
  let budgetFailure;
@@ -1105,6 +1280,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1105
1280
  // every exit, using the existing bounded cleanup and deferred tail.
1106
1281
  yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
1107
1282
  const signal = yield* runSignal(callerSignal);
1283
+ yield* startable(ceiling);
1108
1284
  // Admission comes before provider construction: queued calls retain
1109
1285
  // no catalogs or provider closures.
1110
1286
  let lease;
@@ -1112,7 +1288,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1112
1288
  lease = yield* timed((elapsed) => {
1113
1289
  if (diagnostics)
1114
1290
  diagnostics.admissionMs = elapsed;
1115
- }, leased(executor, signal));
1291
+ }, leased(executor, signal, ceiling));
1116
1292
  if ((lease.waitMs ?? 0) > 0) {
1117
1293
  logger.debug("[connecta] execute_code admitted after queue wait", {
1118
1294
  waitMs: lease.waitMs,
@@ -1137,19 +1313,25 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1137
1313
  maxHostCalls: config.maxHostCalls,
1138
1314
  hostCallTimeoutMs,
1139
1315
  defaultToolTimeoutMs: config.defaultToolTimeoutMs,
1140
- programDeadlineAt: Date.now() + watchdog.ms,
1316
+ programDeadlineAt: ceiling.at,
1317
+ readDeadlineAt: readDeadlineAt(executor, ceiling),
1141
1318
  defer: config.defer,
1142
1319
  readOnly: config.readOnly,
1143
1320
  maxWrites: config.maxWrites,
1144
1321
  routes: config.routes,
1322
+ readProgram: config.readProgram,
1323
+ followUpRoutes: config.followUpRoutes,
1145
1324
  programWrites,
1325
+ readBytes,
1146
1326
  resultDependencies,
1147
1327
  dispatchController,
1148
1328
  unawaited,
1329
+ heldResults,
1149
1330
  }, requestScope)));
1150
1331
  if (signal.aborted) {
1151
1332
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
1152
1333
  }
1334
+ yield* startable(ceiling);
1153
1335
  const admitted = lease;
1154
1336
  // A program write may still be on the wire when the program settles:
1155
1337
  // close, so a write gated after this is not sent, then let the ones
@@ -1169,7 +1351,19 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1169
1351
  interrupted: (logs) => {
1170
1352
  executorLogs = sentSecrets.redact(logs);
1171
1353
  },
1172
- }).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
1354
+ }).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, {
1355
+ watchdog: {
1356
+ ceiling,
1357
+ // With no published deadline, a run that queued has not shown it is wedged.
1358
+ executorDeadlineAt: executor.deadlineMs !== undefined
1359
+ ? Date.now() + executor.deadlineMs
1360
+ : (admitted?.waitMs ?? 0) > 0
1361
+ ? Infinity
1362
+ : undefined,
1363
+ logger: watchdog.logger,
1364
+ },
1365
+ terminal: Deferred.await(terminal),
1366
+ }).pipe(Effect.ensuring(Effect.suspend(() => {
1173
1367
  programWrites.close();
1174
1368
  dispatchController.abort();
1175
1369
  // Finalizers are masked by default. The request must be able
@@ -1193,6 +1387,10 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1193
1387
  maxResultChars - emitted.textChars,
1194
1388
  programWrites.hasSucceeded,
1195
1389
  maxResultChars,
1390
+ heldResults,
1391
+ ceiling.answerBy,
1392
+ readBytes,
1393
+ config.followUpRoutes,
1196
1394
  ]),
1197
1395
  };
1198
1396
  const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
@@ -1200,7 +1398,15 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1200
1398
  // the retained log prefix. Give that report one timer turn, just as
1201
1399
  // cancellation does in awaitExecutor; never wait on a Worker isolate.
1202
1400
  budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
1203
- return Effect.flatMap(exited, (exit) => {
1401
+ // Every way a run answers can carry a result id, so every one of them seals, by the
1402
+ // time the response must be on its way. A sealing cut short leaves entries unreadable.
1403
+ const sealRun = Effect.suspend(() => sealHeldResults(heldResults, registry.resultsStorage(), ceiling.answerBy, callerSignal)).pipe(Effect.tap(({ refused, refusedWrite }) => Effect.sync(() => {
1404
+ if (refused === 0)
1405
+ return;
1406
+ retention.note = refusedWrite ? RESULTS_REFUSED_WRITE_NOTE : RESULTS_REFUSED_NOTE;
1407
+ logFailure(logger, "result paging unavailable", failureRecord({}));
1408
+ })));
1409
+ const answered = Effect.flatMap(exited, (exit) => {
1204
1410
  // A synchronous fire-and-forget burst can also settle its executor in
1205
1411
  // this turn. The host's terminal refusal always wins over that value.
1206
1412
  if (budgetFailure instanceof HostCallBudgetExceeded) {
@@ -1224,6 +1430,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1224
1430
  timeoutError: cancelled,
1225
1431
  }).pipe(Effect.catch((error) => Effect.succeed(failedRun(callerSignal?.aborted ? cancelled : error, logger, reported))));
1226
1432
  });
1433
+ return Effect.tap(answered, () => sealRun);
1227
1434
  });
1228
1435
  return authMethod(({ code, diagnostics }, options = {}) => Effect.suspend(() => {
1229
1436
  const requestScope = config.requestScope ?? {};
@@ -1233,6 +1440,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1233
1440
  return Effect.fail(owner.controller.signal.reason);
1234
1441
  const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
1235
1442
  const programWrites = new ProgramWrites();
1443
+ const unawaited = { read: false };
1444
+ const retention = {};
1445
+ const ceiling = responseCeiling(config.requestStartedAt ?? Date.now(), watchdog.ms);
1236
1446
  const response = Effect.suspend(() => {
1237
1447
  // A code-unit count above the cap is already too large in UTF-8. Check
1238
1448
  // that first so a huge direct-call string is never encoded in full.
@@ -1241,22 +1451,53 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1241
1451
  const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
1242
1452
  return Effect.succeed(failureResponse(message, { code: { code: "invalid_args", message, retryable: false } }));
1243
1453
  }
1244
- return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites);
1454
+ return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling);
1245
1455
  });
1246
1456
  return operationActivity(activity, registry, "program", response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
1247
1457
  // Every response, including intake refusals, crosses accounting and
1248
1458
  // exactly one envelope check at this operation boundary.
1249
- const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished));
1459
+ const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished, sentSecrets, maxResultChars));
1250
1460
  // Calls abandoned by the guest are failed when the run cancels them.
1251
1461
  // Connecta attaches its own integers after the envelope check.
1252
1462
  const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
1253
- result.structuredContent = { ...result.structuredContent, hostCalls: counts };
1463
+ const note = [
1464
+ finishedRunNote(result, hostCalls.attempted, programWrites.sent, unawaited.read),
1465
+ retention.note,
1466
+ ]
1467
+ .filter(Boolean)
1468
+ .join(" ");
1469
+ result.structuredContent = {
1470
+ ...result.structuredContent,
1471
+ ...(note ? { note } : {}),
1472
+ hostCalls: counts,
1473
+ };
1254
1474
  if (result.content[0]?.type === "text")
1255
1475
  result.content[0].text = JSON.stringify(result.structuredContent);
1256
1476
  return result;
1257
- }), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }));
1477
+ }), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }), config.readProgram ? READ_PROGRAM_SOURCE : undefined);
1258
1478
  }));
1259
1479
  }
1480
+ const RESULTS_REFUSED_NOTE = "A result this run retained was not kept for later runs, so paging its id from a new program may fail. Rerun the read for a fresh result.";
1481
+ const RESULTS_REFUSED_WRITE_NOTE = `${RESULTS_REFUSED_NOTE} A write's output among them may not be retrievable: that write already ran, so do not repeat it.`;
1482
+ const UNAWAITED_HINT = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
1483
+ const SENT_NOTE = "Its writes were already sent. Do not rerun it, even with a fix: read their targets in a new program.";
1484
+ /**
1485
+ * A completed run that gave the agent nothing to read looks like nothing
1486
+ * happened, and a write inside it is exactly what must not run again (INV-9).
1487
+ * Fixed text only: attached after the envelope check, like the counters.
1488
+ */
1489
+ function finishedRunNote(result, attempted, wrote, unawaited) {
1490
+ if (result.isError)
1491
+ return undefined;
1492
+ if (unawaited)
1493
+ return wrote ? `${UNAWAITED_HINT} ${SENT_NOTE}` : UNAWAITED_HINT;
1494
+ const body = result.structuredContent;
1495
+ if (body?.result !== undefined || body?.emitted !== undefined)
1496
+ return undefined;
1497
+ if (wrote)
1498
+ return `Program returned nothing. ${SENT_NOTE}`;
1499
+ return `Program returned nothing${attempted > 0 ? ", though its calls ran" : " and made no host calls"}. Add a return.`;
1500
+ }
1260
1501
  /** An execution that never produced an ExecuteResult, as the model sees it. */
1261
1502
  function failedRun(err, logger, { emitted, diagnostics, program }) {
1262
1503
  const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
@@ -1397,67 +1638,87 @@ function failureResponse(message, options) {
1397
1638
  function discardedEmits(emitted) {
1398
1639
  return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
1399
1640
  }
1400
- const inventoryWords = (text) => text
1401
- .toLowerCase()
1402
- .split(/[^\p{L}\p{N}]+/u)
1403
- .filter(Boolean);
1404
- /** A title that only respells the id ("Google Calendar" for google_calendar) adds nothing. */
1405
- function titleAddsInformation(id, title) {
1406
- const words = inventoryWords(title);
1407
- const idWords = inventoryWords(id);
1408
- return words.join("") !== idWords.join("") && words.some((word) => !idWords.includes(word));
1409
- }
1641
+ /** Follows the inventory only when an entry shown carries the mark; `*` cannot appear in an id. */
1642
+ const REQUIRED_GUIDE_LEGEND = ' *Read connecta.skill("connector:<id>") before its first call.';
1643
+ /** Deployment configuration, normalized and capped like a title: a one-line purpose, never a paragraph. */
1644
+ const CONNECTOR_PURPOSE_MAX_BYTES = 57;
1410
1645
  /**
1411
1646
  * Every configured connector, from configuration alone. Detail degrades before
1412
- * names do: titles go first, then optional guide pointers, and only past the
1413
- * byte cap do whole entries give way to an exact omission count.
1647
+ * names do: purposes go first, from the end of registry order, then titles, and
1648
+ * only past the byte cap do whole entries give way to an exact omission count.
1649
+ * Search and describe name optional guides.
1414
1650
  */
1415
1651
  function connectorInventory(connectors) {
1416
1652
  const prefix = "Connectors: ";
1417
1653
  if (connectors.length === 0)
1418
1654
  return `${prefix}none.`;
1419
1655
  const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
1420
- const entry = (connector, detail) => {
1421
- const notes = [];
1422
- const title = connector.title?.replace(/\s+/g, " ").trim();
1423
- if (detail >= 2 && title && titleAddsInformation(connector.id, title))
1424
- notes.push(boundedEchoText(title, 45));
1425
- const required = connectorGuideRequired(connector);
1426
- if (connectorGuide(connector) && (required || detail >= 1)) {
1427
- const fetch = `skills({name:"${connectorSkillName(connector.id)}"})`;
1428
- notes.push(required ? `read first: ${fetch}` : `guide: ${fetch}`);
1429
- }
1430
- return notes.length > 0 ? `${connector.id} (${notes.join("; ")})` : connector.id;
1656
+ const guideFirst = (connector) => connectorGuide(connector) !== undefined && connectorGuideRequired(connector);
1657
+ const legend = (shown) => (shown.some(guideFirst) ? REQUIRED_GUIDE_LEGEND : "");
1658
+ const entry = (connector, titled, withPurpose = false) => {
1659
+ const name = guideFirst(connector) ? `${connector.id}*` : connector.id;
1660
+ const title = titled ? connectorInventoryTitle(connector) : undefined;
1661
+ const description = connector.description?.replace(/\s+/g, " ").trim().replace(/\.$/, "");
1662
+ const shown = `${connector.id} ${connector.title?.replace(/\s+/g, " ").trim() ?? ""}`;
1663
+ const purpose = withPurpose && description && addsInformation(shown, description)
1664
+ ? inventoryNote(description, CONNECTOR_PURPOSE_MAX_BYTES)
1665
+ : undefined;
1666
+ const notes = [title, purpose].filter((note) => note !== undefined);
1667
+ return notes.length > 0 ? `${name} (${notes.join("; ")})` : name;
1431
1668
  };
1432
- for (const detail of [2, 1, 0]) {
1433
- const line = `${prefix}${connectors.map((connector) => entry(connector, detail)).join(", ")}.`;
1434
- if (fits(line))
1435
- return line;
1669
+ const described = (purposes) => `${prefix}${connectors.map((connector, index) => entry(connector, true, index < purposes)).join(", ")}.${legend(connectors)}`;
1670
+ if (fits(described(0))) {
1671
+ // The most entries, in registry order, whose purposes still fit.
1672
+ let shown = 0;
1673
+ let over = connectors.length + 1;
1674
+ while (over - shown > 1) {
1675
+ const middle = Math.floor((shown + over) / 2);
1676
+ if (fits(described(middle)))
1677
+ shown = middle;
1678
+ else
1679
+ over = middle;
1680
+ }
1681
+ return described(shown);
1436
1682
  }
1437
- const entries = connectors.map((connector) => entry(connector, 0));
1683
+ const untitled = `${prefix}${connectors.map((connector) => entry(connector, false)).join(", ")}.${legend(connectors)}`;
1684
+ if (fits(untitled))
1685
+ return untitled;
1686
+ const truncated = (shown) => {
1687
+ const kept = connectors.slice(0, shown);
1688
+ const names = kept.map((connector) => entry(connector, false)).join(", ");
1689
+ return `${prefix}${names}${shown > 0 ? "; " : ""}+${connectors.length - shown} more.${legend(kept)}`;
1690
+ };
1438
1691
  let shown = 0;
1439
- while (fits(`${prefix}${entries.slice(0, shown + 1).join(", ")}; +${entries.length - shown - 1} more.`))
1692
+ while (fits(truncated(shown + 1)))
1440
1693
  shown++;
1441
- return `${prefix}${entries.slice(0, shown).join(", ")}${shown > 0 ? "; " : ""}+${entries.length - shown} more.`;
1694
+ return truncated(shown);
1442
1695
  }
1443
- const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "Writable endpoint: programs may read and write; the client approves."}`} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}.
1696
+ /**
1697
+ * Short by design: Codex carries every description in each turn's tool list and
1698
+ * `execute_code` already holds the full declaration. It still names the guest
1699
+ * calls and the connectors, so a host that loads only this tool can write a
1700
+ * program and tell similar connectors apart.
1701
+ */
1702
+ const READ_PROGRAM_DESCRIPTION = (hostLimits, inventory) => `Run a read-only JavaScript program without write approval: same sandbox and connecta API as ${META_ROUTES.program}, ${hostLimits.maxHostCalls} host calls. Use it for read work at an unknown address, joins, loops and reductions. Each call that can write is refused before dispatch with its next step, and catching the refusal sends nothing. search and describe list those tools with callable: false; ${META_ROUTES.write} and ${META_ROUTES.program} run them. Use async () => { ... } with connecta.search({ query }), connecta.describe({ address }), connecta.call(address, args), connecta.result(resultId, { page }), and return the answer.
1703
+ ${inventory}`;
1704
+ const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES, readRoute = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may write; the host approves."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : readRoute ? `One known read: ${routes.read}. One known write: ${routes.write}. Read programs: ${READ_PROGRAM_SOURCE}. Other: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "Writable endpoint: programs may write; the host approves."}`} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}${hostLimits.runDeadlineMs === undefined ? "" : `, ${hostLimits.runDeadlineMs / 1_000}s/run`}.
1444
1705
  ${connectorInventory(connectors)}
1445
1706
 
1446
- Use async () => { ... }, no arguments. No portable ambient capabilities. API:
1707
+ Use async () => { ... }, no arguments. No imports, require, fetch, filesystem or timers. API:
1447
1708
  \`\`\`ts
1448
1709
  ${GUEST_API_DECLARATION}
1449
1710
  \`\`\`
1450
- search and describe return objects, not arrays. Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle; read it with connecta.result(handle, { offset }), which pages by UTF-8 byte offset. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call. Promise.allSettled keeps per-call errors, but exceeding the host-call limit ends the program. When paging is available, an oversized program return carries resultId and nextAction.
1711
+ search and describe return objects, not arrays.${connectorGuides ? " A row with guideRequired: read its connector guide first." : ""} Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle: follow its hint. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call; over the limit the program returns nothing, even under Promise.allSettled. Near the host-call limit, return page.nextCursor to resume next program. An oversized return carries nextAction.
1451
1712
 
1452
- Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks unchanged; an authored Block's image/audio data is base64. emit needs no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. connecta.skill("usage"): repair${connectorGuides ? ", guide handling" : ""}.`;
1713
+ ${readOnly ? "" : "Never repeat writes for output. "}emit(result) or emit(block) forwards MCP blocks; authored image/audio data is base64. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; text shares the return cap. connecta.skill("usage"): repair.`;
1453
1714
  // Module scope, like the other five meta-tool inputs: its JSON Schema is
1454
1715
  // derived once per process. Budgets and connectors vary by deployment and
1455
1716
  // view, so they live in the description, never here.
1456
1717
  const EXECUTE_INPUT = advertisedSchema(z.object({
1457
1718
  code: z
1458
1719
  .string()
1459
- .describe("One complete zero-argument JavaScript async arrow: async () => { ... }. Use the provided connecta global to discover, call, and return the reduced answer. At most 65,536 UTF-8 bytes."),
1460
- diagnostics: z.boolean().optional().describe("Add request-local, payload-free timing and result-size summaries."),
1720
+ .describe("Source text of one zero-argument JavaScript async arrow: async () => { ...; return answer; }. Pass a string, even from a JavaScript host. Use the connecta global. At most 65,536 UTF-8 bytes."),
1721
+ diagnostics: z.boolean().optional().describe("Add timing and result-size summaries."),
1461
1722
  }));
1462
1723
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
1463
1724
  export function registerExecuteTool(server, registry, ctx) {
@@ -1475,7 +1736,7 @@ export function registerExecuteTool(server, registry, ctx) {
1475
1736
  };
1476
1737
  const connectors = registry.listConnectors();
1477
1738
  const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
1478
- const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
1739
+ const runnerConfig = {
1479
1740
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1480
1741
  discoveryConcurrency: ctx.discoveryConcurrency,
1481
1742
  probeTimeoutMs: ctx.probeTimeoutMs,
@@ -1489,23 +1750,11 @@ export function registerExecuteTool(server, registry, ctx) {
1489
1750
  maxWrites,
1490
1751
  maxResultChars: ctx.maxResultChars,
1491
1752
  requestScope: ctx.requestScope,
1753
+ requestStartedAt: ctx.requestStartedAt,
1492
1754
  routes: ctx.routes,
1493
- });
1494
- server.registerTool("execute_code", {
1495
- description: executeDescription(emitBudgets, {
1496
- ...hostLimits,
1497
- // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1498
- callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1499
- }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
1500
- inputSchema: EXECUTE_INPUT,
1501
- outputSchema: EXECUTE_OUTPUT,
1502
- // The client sees execute_code as a write exactly when the endpoint writes.
1503
- annotations: {
1504
- readOnlyHint: ctx.readOnly === true,
1505
- destructiveHint: ctx.readOnly !== true,
1506
- openWorldHint: true,
1507
- },
1508
- }, authMethod((args, extra) => Effect.gen(function* () {
1755
+ ...(ctx.readRoute ? { followUpRoutes: routeNames("read-route") } : {}),
1756
+ };
1757
+ const programHandler = (execute, name) => (args, extra) => Effect.gen(function* () {
1509
1758
  if (ctx.client)
1510
1759
  bindMcpClient(extra, ctx.client, ctx.activity);
1511
1760
  const run = authMethod(() => Effect.suspend(() => {
@@ -1529,8 +1778,35 @@ export function registerExecuteTool(server, registry, ctx) {
1529
1778
  })));
1530
1779
  }));
1531
1780
  const result = yield* ctx.authElicitation
1532
- ? authInvoke(ctx.authElicitation, "run", ["execute_code", args, extra, run])
1781
+ ? authInvoke(ctx.authElicitation, "run", [name, args, extra, run])
1533
1782
  : authCall(run, []);
1534
1783
  return ctx.native ? nativeRecovery(result) : result;
1535
- })));
1784
+ });
1785
+ const newRunner = (config) => createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, config);
1786
+ server.registerTool("execute_code", {
1787
+ description: executeDescription(emitBudgets, {
1788
+ ...hostLimits,
1789
+ // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1790
+ callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1791
+ ...(ctx.executor.deadlineMs !== undefined
1792
+ ? { runDeadlineMs: Math.min(ctx.executor.deadlineMs, resolveBudget(ctx.watchdogMs, EXECUTE_WATCHDOG_MS)) }
1793
+ : {}),
1794
+ }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes, ctx.readRoute === true),
1795
+ inputSchema: EXECUTE_INPUT,
1796
+ outputSchema: EXECUTE_OUTPUT,
1797
+ // The client sees execute_code as a write exactly when the endpoint writes.
1798
+ annotations: {
1799
+ readOnlyHint: ctx.readOnly === true,
1800
+ destructiveHint: ctx.readOnly !== true,
1801
+ openWorldHint: true,
1802
+ },
1803
+ }, authMethod(programHandler(newRunner(runnerConfig), "execute_code")));
1804
+ if (ctx.readRoute) {
1805
+ server.registerTool(READ_PROGRAM_SOURCE, {
1806
+ description: READ_PROGRAM_DESCRIPTION(hostLimits, connectorInventory(connectors)),
1807
+ inputSchema: EXECUTE_INPUT,
1808
+ outputSchema: EXECUTE_OUTPUT,
1809
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1810
+ }, authMethod(programHandler(newRunner({ ...runnerConfig, readProgram: true, routes: routeNames("read-route") }), READ_PROGRAM_SOURCE)));
1811
+ }
1536
1812
  }