@zackbart/connecta 0.25.0 → 0.26.1

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 (100) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +387 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/artifacts/connector.d.ts +55 -0
  9. package/dist/artifacts/connector.js +724 -0
  10. package/dist/artifacts/document.js +80 -0
  11. package/dist/artifacts/guide.js +140 -0
  12. package/dist/artifacts/html-scan.js +329 -0
  13. package/dist/artifacts/json.js +68 -0
  14. package/dist/artifacts/kv-store.d.ts +20 -0
  15. package/dist/artifacts/kv-store.js +140 -0
  16. package/dist/artifacts/markdown.js +478 -0
  17. package/dist/artifacts/module.d.ts +37 -0
  18. package/dist/artifacts/module.js +60 -0
  19. package/dist/artifacts/operations.d.ts +195 -0
  20. package/dist/artifacts/operations.js +944 -0
  21. package/dist/artifacts/refresh.d.ts +53 -0
  22. package/dist/artifacts/refresh.js +235 -0
  23. package/dist/artifacts/routes.js +217 -0
  24. package/dist/artifacts/types.d.ts +214 -0
  25. package/dist/artifacts/types.js +22 -0
  26. package/dist/artifacts/validate.d.ts +62 -0
  27. package/dist/artifacts/validate.js +653 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/auth/downstream-oauth.js +397 -48
  31. package/dist/auth/oauth-start-reset.js +20 -0
  32. package/dist/branding.d.ts +87 -0
  33. package/dist/branding.js +170 -1
  34. package/dist/catalog-service.js +39 -29
  35. package/dist/catalog.js +42 -8
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connectors/remote-mcp.js +48 -9
  41. package/dist/errors.d.ts +16 -1
  42. package/dist/errors.js +28 -0
  43. package/dist/execute.js +431 -75
  44. package/dist/executor-admission.d.ts +11 -1
  45. package/dist/executor-admission.js +79 -14
  46. package/dist/executors/quickjs.js +4 -2
  47. package/dist/index.d.ts +62 -6
  48. package/dist/index.js +230 -6
  49. package/dist/invocation.js +56 -10
  50. package/dist/meta-tools.js +7 -2
  51. package/dist/module-contracts.d.ts +21 -1
  52. package/dist/oauth-callback-outcome.js +43 -18
  53. package/dist/operator-ui/generated.js +2 -2
  54. package/dist/operator-ui/model.d.ts +62 -5
  55. package/dist/operator-ui/view.js +372 -28
  56. package/dist/page-styles.js +7 -0
  57. package/dist/program-source.js +116 -0
  58. package/dist/registry.d.ts +42 -6
  59. package/dist/registry.js +164 -60
  60. package/dist/resumable.js +1291 -0
  61. package/dist/routes/activity.js +6 -69
  62. package/dist/routes/actor-labels.js +114 -0
  63. package/dist/routes/credentials.js +27 -5
  64. package/dist/routes/mcp.js +120 -59
  65. package/dist/routes/oauth-management.js +87 -20
  66. package/dist/routes/oauth.js +54 -87
  67. package/dist/routes/operator.js +1 -0
  68. package/dist/routes/shared.d.ts +23 -1
  69. package/dist/routes/shared.js +20 -2
  70. package/dist/routes/ui-data.js +2 -1
  71. package/dist/routes/ui.js +19 -4
  72. package/dist/run-journal.js +347 -0
  73. package/dist/runtime/shared-read.js +106 -0
  74. package/dist/server.js +30 -2
  75. package/dist/skills.js +30 -11
  76. package/dist/timeout.js +26 -0
  77. package/dist/tool-safety.d.ts +35 -0
  78. package/dist/tool-safety.js +31 -0
  79. package/dist/types.d.ts +23 -0
  80. package/dist/ui.d.ts +17 -5
  81. package/dist/ui.js +51 -68
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/documentation/architecture.md +214 -27
  85. package/documentation/auth.md +186 -2
  86. package/documentation/code-mode.md +236 -63
  87. package/documentation/meta-tools.md +55 -29
  88. package/ethos.md +12 -10
  89. package/examples/worker/AGENTS.md +6 -0
  90. package/examples/worker/README.md +174 -10
  91. package/examples/worker/src/d1-activity-row.ts +14 -2
  92. package/examples/worker/src/d1-activity.ts +4 -2
  93. package/examples/worker/src/index.ts +36 -1
  94. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  95. package/examples/worker/wrangler.jsonc +5 -1
  96. package/package.json +6 -2
  97. package/templates/node/AGENTS.md +9 -2
  98. package/templates/node/README.md +31 -3
  99. package/templates/node/package.json +1 -1
  100. package/templates/node/src/index.ts +27 -0
package/dist/execute.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Cause, Duration, Effect, Exit } from "effect";
1
+ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
2
2
  import { z } from "zod";
3
3
  import { advertisedSchema } from "./advertised-schema.js";
4
4
  import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
@@ -7,12 +7,18 @@ import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, }
7
7
  import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
8
8
  import { boundedEchoText, msg } from "./errors.js";
9
9
  import { InvocationFailure, InvocationService, timed, } from "./invocation.js";
10
- import { withoutProgramTerminator } from "./program-source.js";
10
+ import { normalizeProgramSource } from "./program-source.js";
11
+ import { DEFAULT_MAX_WRITES, ExemptWrites, MIN_STORAGE_TIMEOUT_MS, RunState, writeStateOf, } from "./resumable.js";
12
+ import { journalKey } from "./run-journal.js";
13
+ import { underAnySignal } from "./timeout.js";
14
+ import { isApprovalExempt, NO_EXEMPTIONS, } from "./tool-safety.js";
11
15
  import { fromSignal, runEdge } from "./runtime/run.js";
12
16
  import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, } from "./skills.js";
13
17
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
14
18
  const EXECUTE_MAX_HOST_CALLS = 20;
15
19
  const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
20
+ /** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
21
+ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
16
22
  /**
17
23
  * The outer ceiling on one execution. It sits above both executors' own
18
24
  * deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
@@ -272,6 +278,129 @@ function guestErrorPrelude(failureSecret) {
272
278
  });
273
279
  })(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
274
280
  }
281
+ /** A clock read now and a seed drawn from the host's CSPRNG. */
282
+ function freshEnvironment() {
283
+ const words = new Uint32Array(4);
284
+ crypto.getRandomValues(words);
285
+ return {
286
+ clockMs: Date.now(),
287
+ seed: [words[0] ?? 0, words[1] ?? 0, words[2] ?? 0, words[3] ?? 0],
288
+ };
289
+ }
290
+ /**
291
+ * Trusted guest code installing the pinned clock and stream, evaluated after
292
+ * the error prelude and before the program. Every replacement is locked the
293
+ * way `Error` is — non-writable and non-configurable — so a program can read
294
+ * the pinned values and nothing else.
295
+ *
296
+ * `Date` becomes a wrapper whose argument-free forms (`new Date()`, `Date()`,
297
+ * `Date.now()`) read the pinned instant; with arguments it is the native
298
+ * constructor, and instances are ordinary dates. The native constructor's own
299
+ * `now` and the prototype's `constructor` are pinned too, so the usual ways
300
+ * back to it read the same instant. `Math.random` is sfc32 over the seed:
301
+ * small, fast, and specified in a few lines anyone can check. Where the
302
+ * runtime has them (a Dynamic Worker; QuickJS has neither), `crypto`'s
303
+ * `getRandomValues` and `randomUUID` draw from the same stream and
304
+ * `performance.now()` stays at 0, the run's start. None of that makes
305
+ * `crypto` contract (`P2`): it only keeps a Workers-only program from being
306
+ * the one that cannot replay.
307
+ */
308
+ function pinnedEnvironmentPrelude(environment) {
309
+ return `((clock, seed) => {
310
+ const NativeDate = globalThis.Date;
311
+ const construct = Reflect.construct;
312
+ const defineProperty = Object.defineProperty;
313
+ const lock = (target, key, value) => defineProperty(target, key, {
314
+ value, writable: false, enumerable: false, configurable: false
315
+ });
316
+ let a = seed[0] | 0, b = seed[1] | 0, c = seed[2] | 0, d = seed[3] | 0;
317
+ const next = () => {
318
+ const t = (((a + b) | 0) + d) | 0;
319
+ d = (d + 1) | 0;
320
+ a = b ^ (b >>> 9);
321
+ b = (c + (c << 3)) | 0;
322
+ c = (c << 21) | (c >>> 11);
323
+ c = (c + t) | 0;
324
+ return t >>> 0;
325
+ };
326
+ const now = function now() { return clock; };
327
+ function Date(...args) {
328
+ if (new.target === undefined) return new NativeDate(clock).toString();
329
+ return construct(NativeDate, args.length === 0 ? [clock] : args, new.target);
330
+ }
331
+ Date.prototype = NativeDate.prototype;
332
+ lock(Date, "now", now);
333
+ lock(Date, "parse", NativeDate.parse);
334
+ lock(Date, "UTC", NativeDate.UTC);
335
+ lock(NativeDate, "now", now);
336
+ lock(NativeDate.prototype, "constructor", Date);
337
+ lock(globalThis, "Date", Date);
338
+ lock(Math, "random", function random() { return next() / 4294967296; });
339
+ // Lock the replacement on the object and on its prototype, where the
340
+ // native method lives: \`Object.getPrototypeOf(crypto).getRandomValues\`
341
+ // would otherwise still reach the real stream. A prototype the runtime
342
+ // will not let us redefine keeps its native method (\`tryLock\`), and a
343
+ // program that uses it diverges on replay rather than sending anything.
344
+ const tryLock = (target, key, value) => {
345
+ try { lock(target, key, value); } catch {}
346
+ };
347
+ if (typeof crypto === "object" && crypto !== null) {
348
+ const getRandomValues = function getRandomValues(array) {
349
+ const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
350
+ for (let i = 0; i < bytes.length; i++) bytes[i] = next() >>> 24;
351
+ return array;
352
+ };
353
+ const randomUUID = function randomUUID() {
354
+ const bytes = getRandomValues(new Uint8Array(16));
355
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
356
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
357
+ let hex = "";
358
+ for (let i = 0; i < 16; i++) {
359
+ hex += (bytes[i] + 0x100).toString(16).slice(1);
360
+ if (i === 3 || i === 5 || i === 7 || i === 9) hex += "-";
361
+ }
362
+ return hex;
363
+ };
364
+ lock(crypto, "getRandomValues", getRandomValues);
365
+ lock(crypto, "randomUUID", randomUUID);
366
+ const cryptoProto = Object.getPrototypeOf(crypto);
367
+ if (cryptoProto) {
368
+ tryLock(cryptoProto, "getRandomValues", getRandomValues);
369
+ tryLock(cryptoProto, "randomUUID", randomUUID);
370
+ }
371
+ }
372
+ if (typeof performance === "object" && performance !== null) {
373
+ const perfNow = function now() { return 0; };
374
+ lock(performance, "now", perfNow);
375
+ const performanceProto = Object.getPrototypeOf(performance);
376
+ if (performanceProto) tryLock(performanceProto, "now", perfNow);
377
+ }
378
+ // An Intl formatter asked for "now" (no date) reads the native clock.
379
+ if (typeof Intl === "object" && Intl !== null && typeof Intl.DateTimeFormat === "function") {
380
+ const proto = Intl.DateTimeFormat.prototype;
381
+ const formatGetter = Object.getOwnPropertyDescriptor(proto, "format");
382
+ if (formatGetter && typeof formatGetter.get === "function") {
383
+ const nativeFormat = formatGetter.get;
384
+ try {
385
+ defineProperty(proto, "format", {
386
+ get() {
387
+ const bound = nativeFormat.call(this);
388
+ return function format(date) { return bound(date === undefined ? clock : date); };
389
+ },
390
+ enumerable: false,
391
+ configurable: false,
392
+ });
393
+ } catch {}
394
+ }
395
+ const nativeFormatToParts = proto.formatToParts;
396
+ if (typeof nativeFormatToParts === "function") {
397
+ tryLock(proto, "formatToParts", function formatToParts(date) {
398
+ return nativeFormatToParts.call(this, date === undefined ? clock : date);
399
+ });
400
+ }
401
+ }
402
+ })(${JSON.stringify(environment.clockMs)}, ${JSON.stringify([...environment.seed])});`;
403
+ }
275
404
  /**
276
405
  * Expose one host provider and typed guest errors. Catalogs load only when
277
406
  * the program calls a tool or asks search/describe.
@@ -300,6 +429,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
300
429
  concurrency: limits.discoveryConcurrency,
301
430
  probeTimeoutMs: limits.probeTimeoutMs,
302
431
  defer: limits.defer,
432
+ approval: limits.approval,
303
433
  });
304
434
  const invocation = new InvocationService(registry, catalog, activity);
305
435
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
@@ -312,27 +442,135 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
312
442
  const spendHostCall = Effect.suspend(() => ++hostCalls > maxHostCalls
313
443
  ? Effect.fail(guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`))
314
444
  : Effect.void);
315
- const invocationContext = () => ({
316
- source: "execute_code",
445
+ const { runState, exemptWrites } = limits;
446
+ const approval = limits.approval ?? NO_EXEMPTIONS;
447
+ const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
448
+ let exemptWriteCount = 0;
449
+ /**
450
+ * Without resumable writes nothing can pause, but a config exemption
451
+ * (#566) still lets its writes run: the gate covers exactly the exempt
452
+ * calls, spends the write budget on each, and every other write keeps
453
+ * E4's refusal, ahead of validation as it always was. A dispatched write
454
+ * is tracked until it settles (`ExemptWrites`), so the play can wait for
455
+ * it rather than abort it.
456
+ */
457
+ const exemptOnly = (sending) => ({
458
+ gates: (target) => isApprovalExempt(approval, target.connector, target.toolName, target.definition),
459
+ writeGate: (target) => Effect.sync(() => {
460
+ if (exemptWrites?.isClosed) {
461
+ return {
462
+ kind: "refuse",
463
+ error: {
464
+ code: "cancelled",
465
+ message: "The program had already returned, so this write was not sent.",
466
+ retryable: false,
467
+ },
468
+ activity: "none",
469
+ };
470
+ }
471
+ if (exemptWriteCount >= maxWrites) {
472
+ return {
473
+ kind: "refuse",
474
+ error: {
475
+ code: "budget_exceeded",
476
+ message: `execute_code write budget exceeded (${maxWrites} writes maximum, execute.maxWrites); ${target.connector.id}.${target.toolName} was not sent`,
477
+ retryable: false,
478
+ },
479
+ };
480
+ }
481
+ exemptWriteCount++;
482
+ if (exemptWrites)
483
+ sending.settle = exemptWrites.begin();
484
+ return { kind: "dispatch" };
485
+ }),
486
+ });
487
+ const invocationContext = (seq, key, sending) => ({
488
+ source: runState?.source ?? "execute_code",
317
489
  timeoutMs: hostCallTimeoutMs,
318
490
  ...(signal !== undefined ? { requestSignal: signal } : {}),
319
491
  unwrapResult: true,
492
+ // With resumable writes, a consequential call reaches the run's gate
493
+ // instead of the flat E4 refusal, and a read's decision is marked the
494
+ // moment it passes the same point.
495
+ ...(runState && seq !== undefined && key !== undefined
496
+ ? {
497
+ beforeDispatch: () => runState.decide(seq),
498
+ writeGate: (target, args) => runState.gate(seq, key, target, args),
499
+ }
500
+ : runState
501
+ ? {}
502
+ : exemptOnly(sending)),
503
+ });
504
+ /**
505
+ * The front of every numbered host call once resumable writes are on: a
506
+ * stopped run makes no call at all (not journaled, no activity), a
507
+ * recorded one is answered from the journal before the budget-free part
508
+ * of the path, and one the journal cannot account for is divergence.
509
+ * Replayed calls spend the host-call budget again, so a play's total is
510
+ * the run's total and nothing is counted twice.
511
+ */
512
+ const fromJournal = (seq, op, address, args) => Effect.gen(function* () {
513
+ const halted = runState?.halted();
514
+ if (halted)
515
+ return yield* Effect.fail(halted);
516
+ yield* spendHostCall;
517
+ if (!runState || seq === undefined)
518
+ return { kind: "live", key: undefined };
519
+ const key = journalKey(op, address, args);
520
+ const found = runState.lookup(key);
521
+ if (found.kind === "diverged") {
522
+ return yield* Effect.fail(runState.diverge(found.reason));
523
+ }
524
+ if (found.kind === "live")
525
+ return { kind: "live", key };
526
+ runState.decide(seq);
527
+ const { outcome } = found.entry;
528
+ return outcome.ok
529
+ ? { kind: "replayed", value: outcome.value }
530
+ : yield* Effect.fail(new InvocationFailure(outcome.error));
320
531
  });
321
532
  // A call brings its own cancellation: the invocation pipeline reads the
322
533
  // run's signal, refuses a call that starts after it, and records the
323
534
  // cancelled attempt in activity like any other outcome.
324
- const call = (address, args) => spendHostCall.pipe(Effect.andThen(Effect.suspend(() => invocation.pipeline(String(address), args ?? {}, invocationContext()))), Effect.flatMap((outcome) => {
535
+ const call = (seq, address, args) => Effect.gen(function* () {
536
+ const addressText = String(address);
537
+ const callArgs = args ?? {};
538
+ const front = yield* fromJournal(seq, "call", addressText, callArgs);
539
+ if (front.kind === "replayed")
540
+ return front.value;
541
+ // An exempt write dispatched without a run state settles here —
542
+ // unknown if the call never returned an outcome.
543
+ const sending = {};
544
+ const outcome = yield* invocation.pipeline(addressText, callArgs, invocationContext(seq, front.key, sending)).pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
325
545
  diagnostics?.recordCall(outcome);
546
+ if (runState && seq !== undefined && front.key !== undefined) {
547
+ if (runState.isLiveWrite(seq)) {
548
+ const resolved = outcome.resolved;
549
+ const replaced = yield* runState.settleWrite(seq, front.key, {
550
+ address: resolved
551
+ ? `${resolved.connector.id}.${resolved.toolName}`
552
+ : addressText,
553
+ args: callArgs,
554
+ }, outcome);
555
+ if (replaced)
556
+ return yield* Effect.fail(replaced);
557
+ }
558
+ else {
559
+ runState.record(seq, "call", front.key, outcome.ok
560
+ ? { ok: true, value: outcome.value }
561
+ : { ok: false, error: outcome.error });
562
+ }
563
+ }
326
564
  return outcome.ok
327
- ? Effect.succeed(outcome.value)
328
- : Effect.fail(new InvocationFailure(outcome.error));
329
- }));
565
+ ? outcome.value
566
+ : yield* Effect.fail(new InvocationFailure(outcome.error));
567
+ });
330
568
  // Discovery gets the same treatment from here (L2): once the run has
331
569
  // ended no search or describe starts, and one still in flight fails
332
570
  // `cancelled` instead of holding the program until its probe deadline.
333
571
  // Policy failures use the same thrown vocabulary as calls and utilities;
334
572
  // the transport below reconstructs their code inside the guest.
335
- const discovery = (operation, read) => Effect.suspend(() => {
573
+ const discovery = (operation, seq, raw, read) => Effect.suspend(() => {
336
574
  const started = Date.now();
337
575
  const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
338
576
  const reading = Effect.tryPromise({
@@ -341,22 +579,49 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
341
579
  ? guestFailure(err.code, err.message)
342
580
  : err,
343
581
  });
344
- return spendHostCall.pipe(Effect.andThen(!signal
345
- ? reading
346
- : signal.aborted
347
- ? Effect.fail(cancelled())
348
- : Effect.raceAllFirst([
349
- reading,
350
- fromSignal(signal).pipe(Effect.mapError(cancelled)),
351
- ])), Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
582
+ return fromJournal(seq, operation, "", raw ?? {}).pipe(Effect.flatMap((front) => {
583
+ if (front.kind === "replayed")
584
+ return Effect.succeed(front.value);
585
+ const live = (!signal
586
+ ? reading
587
+ : signal.aborted
588
+ ? Effect.fail(cancelled())
589
+ : Effect.raceAllFirst([
590
+ reading,
591
+ fromSignal(signal).pipe(Effect.mapError(cancelled)),
592
+ ])).pipe(Effect.onExit((exit) => Effect.sync(() => {
593
+ if (!runState || seq === undefined || front.key === undefined)
594
+ return;
595
+ // Typed outcomes replay; an untyped throw is a bug, not an
596
+ // answer, and is not journaled — so a replay that re-issues
597
+ // the call before repeating the approved write finds no record
598
+ // and fails `execution_diverged`.
599
+ if (Exit.isSuccess(exit)) {
600
+ runState.record(seq, operation, front.key, {
601
+ ok: true,
602
+ value: exit.value,
603
+ });
604
+ return;
605
+ }
606
+ const error = Cause.squash(exit.cause);
607
+ if (error instanceof InvocationFailure) {
608
+ runState.record(seq, operation, front.key, {
609
+ ok: false,
610
+ error: error.details,
611
+ });
612
+ }
613
+ })));
614
+ return live;
615
+ }), Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
352
616
  });
353
617
  const operations = {
354
618
  call,
355
619
  // Emission is a provider function, never an ExecuteResult field —
356
620
  // that is what keeps the Executor contract untouched and parity
357
621
  // structural (M8). It spends no host-call budget (M7); its own
358
- // budgets live in the collector.
359
- emit: (block) => Effect.try({
622
+ // budgets live in the collector. It is never journaled: a replay emits
623
+ // again, and only the play that completes delivers.
624
+ emit: (_seq, block) => Effect.try({
360
625
  try: () => {
361
626
  if (!limits.emitCollector) {
362
627
  throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
@@ -365,7 +630,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
365
630
  },
366
631
  catch: (err) => err,
367
632
  }),
368
- search: (raw) => discovery("search", async () => {
633
+ search: (seq, raw) => discovery("search", seq, raw, async () => {
369
634
  const args = (raw ?? {});
370
635
  const result = flatSearchResult(await catalog.search({
371
636
  ...args,
@@ -374,7 +639,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
374
639
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
375
640
  return result;
376
641
  }),
377
- describe: (raw) => discovery("describe", async () => {
642
+ describe: (seq, raw) => discovery("describe", seq, raw, async () => {
378
643
  const args = (raw ?? {});
379
644
  const result = { tools: await catalog.describe(args) };
380
645
  boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
@@ -394,11 +659,22 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
394
659
  });
395
660
  return {
396
661
  name: "connecta",
397
- prelude: guestErrorPrelude(failureSecret),
662
+ // Typed errors first, then the pinned clock and randomness: both are
663
+ // trusted host code the program runs after and cannot undo.
664
+ prelude: `${guestErrorPrelude(failureSecret)}\n${pinnedEnvironmentPrelude(runState?.environment ?? limits.environment ?? freshEnvironment())}`,
398
665
  fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
399
666
  name,
400
667
  (...args) => {
401
- const settled = runEdge(Effect.suspend(() => operation(...args)).pipe(Effect.catch(framed)));
668
+ // Numbered here, synchronously, as the program makes the call: the
669
+ // numbering is the program's own issue order, which is what makes
670
+ // a pause point and a replay reproducible.
671
+ const seq = runState && name !== "emit" ? runState.begin() : undefined;
672
+ const settled = runEdge(Effect.suspend(() => operation(seq, ...args)).pipe(Effect.catch(framed), Effect.ensuring(Effect.sync(() => {
673
+ if (seq !== undefined)
674
+ runState?.decide(seq);
675
+ }))));
676
+ if (seq !== undefined)
677
+ runState?.track(seq, settled);
402
678
  // The executor owns this promise, and a program may abandon a call
403
679
  // that the run's end then cancels before anyone has awaited it.
404
680
  // The rejection is still there for whoever does; it is just not an
@@ -494,24 +770,40 @@ function leased(executor, signal) {
494
770
  late: (lease) => lease.release(),
495
771
  }), (lease) => Effect.sync(() => lease.release()));
496
772
  }
497
- /** The execute_code handler. Exported for direct testing. */
498
- export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
773
+ /**
774
+ * Room a claim leaves beyond the watchdog and one host-call deadline, for the
775
+ * executor queue a resumed play waits in first. Only liveness rides on it: a
776
+ * claim that lapses early lets a takeover start, and the write-ahead mark
777
+ * still keeps any write from being sent twice.
778
+ */
779
+ const CLAIM_SLACK_MS = 10_000;
780
+ /** The runner behind both program tools. */
781
+ export function createProgramRunner(registry, baseUrl, executor, logger, activity, config = {}) {
499
782
  const watchdog = {
500
783
  ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
501
784
  logger,
502
785
  };
503
- return ({ code, diagnostics: diagnosticsRequested }, options = {}) => runEdge(Effect.suspend(() => {
786
+ const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
787
+ const storageTimeoutMs = resolveBudget(config.storageTimeoutMs, Math.max(hostCallTimeoutMs, MIN_STORAGE_TIMEOUT_MS));
788
+ /**
789
+ * One play of a program. Without a run state it is the execute_code run it
790
+ * always was. With one, the play also ends when the run stops — a pause or
791
+ * a typed failure — and then waits for what is still on the wire, persists
792
+ * what it must, and discards the program's own result.
793
+ */
794
+ const play = (program, runState, diagnosticsRequested, callerSignal) => Effect.suspend(() => {
504
795
  const diagnostics = diagnosticsRequested
505
796
  ? new ExecuteDiagnostics()
506
797
  : undefined;
507
798
  const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
508
799
  const invocationFailures = [];
800
+ const exemptWrites = runState ? undefined : new ExemptWrites();
509
801
  // The run's scope holds its signal and its lease. However the run
510
802
  // ends — a result, a thrown executor, the watchdog, cancellation —
511
803
  // closing it releases the lease and then aborts the signal, so
512
804
  // nothing the run started outlives the request.
513
805
  const run = Effect.gen(function* () {
514
- const signal = yield* runSignal(options.signal);
806
+ const signal = yield* runSignal(callerSignal);
515
807
  // Admission comes before provider construction: queued calls retain
516
808
  // no catalogs, request scopes, or provider closures.
517
809
  let lease;
@@ -541,28 +833,103 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
541
833
  discoveryConcurrency: config.discoveryConcurrency,
542
834
  probeTimeoutMs: config.probeTimeoutMs,
543
835
  maxHostCalls: config.maxHostCalls,
544
- hostCallTimeoutMs: config.hostCallTimeoutMs,
836
+ hostCallTimeoutMs,
545
837
  defer: config.defer,
838
+ environment: config.environment,
839
+ runState,
840
+ approval: config.approval,
841
+ maxWrites: config.maxWrites,
842
+ exemptWrites,
546
843
  })));
547
844
  if (signal.aborted) {
548
845
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
549
846
  }
550
847
  const admitted = lease;
551
- // P1: a trailing `;` after the arrow expression breaks both
552
- // executors' parenthesized evaluation. Strip it here, above them.
553
- const program = withoutProgramTerminator(code);
554
- return yield* timed((elapsed) => {
848
+ const executing = awaitExecutor(() => admitted
849
+ ? admitted.execute(program, [provider])
850
+ : executor.execute(program, [provider]), signal, { watchdog }).pipe(Effect.map((settled) => ({ settled })));
851
+ if (!runState) {
852
+ // Nothing pauses, but an exempt write may be on the wire: close,
853
+ // then let it finish before the scope aborts it.
854
+ return yield* timed((elapsed) => {
855
+ if (diagnostics)
856
+ diagnostics.executorWallMs = elapsed;
857
+ }, executing.pipe(Effect.ensuring(Effect.suspend(() => {
858
+ exemptWrites?.close();
859
+ return exemptWrites?.drain() ?? Effect.void;
860
+ }))));
861
+ }
862
+ // The run stopping wins the race as soon as its gate decides, before
863
+ // the program even sees the rejection. Either way the play closes —
864
+ // a write gated after this is not sent — and what is still on the
865
+ // wire finishes before the scope aborts it: every call after a stop,
866
+ // since each is journaled, and every write otherwise, since an
867
+ // abandoned write would be an unknown one.
868
+ const ended = yield* timed((elapsed) => {
555
869
  if (diagnostics)
556
870
  diagnostics.executorWallMs = elapsed;
557
- }, awaitExecutor(() => admitted
558
- ? admitted.execute(program, [provider])
559
- : executor.execute(program, [provider]), signal, { watchdog }));
871
+ }, Effect.raceFirst(executing, Deferred.await(runState.stopped).pipe(Effect.map((stop) => ({ stop })))).pipe(Effect.ensuring(Effect.suspend(() => {
872
+ runState.close();
873
+ return runState.drain("writes");
874
+ }))));
875
+ if ("stop" in ended)
876
+ yield* runState.drain("all");
877
+ return ended;
560
878
  });
561
879
  const reported = { emitted, diagnostics, invocationFailures };
562
- return Effect.map(Effect.exit(Effect.scoped(run)), (exit) => Exit.isSuccess(exit)
563
- ? finishedRun(exit.value, reported)
564
- : failedRun(Cause.squash(exit.cause), logger, reported));
565
- }));
880
+ return Effect.flatMap(Effect.exit(Effect.scoped(run)), (exit) => {
881
+ if (Exit.isFailure(exit)) {
882
+ const failed = failedRun(Cause.squash(exit.cause), logger, reported);
883
+ return runState
884
+ ? runState.finishExecutorFailure(failed)
885
+ : Effect.succeed(exemptWrites?.finish(failed) ?? failed);
886
+ }
887
+ const ended = exit.value;
888
+ if ("stop" in ended) {
889
+ return runState
890
+ ? runState.finishStopped()
891
+ : Effect.die(new Error("a run without state cannot stop"));
892
+ }
893
+ const finished = finishedRun(ended.settled, reported);
894
+ if (config.failOnInvocationFailure && invocationFailures.length > 0) {
895
+ const refusal = invocationFailures[0];
896
+ return Effect.succeed(failureResponse(refusal.details.message, {
897
+ code: refusal.details,
898
+ }));
899
+ }
900
+ return runState
901
+ ? runState.finishSettled(finished)
902
+ : Effect.succeed(exemptWrites?.finish(finished) ?? finished);
903
+ });
904
+ });
905
+ return {
906
+ claimMs: watchdog.ms + hostCallTimeoutMs + CLAIM_SLACK_MS,
907
+ storageTimeoutMs,
908
+ execute: ({ code, diagnostics }, options = {}) => {
909
+ // A code-unit count above the cap is already too large in UTF-8. Check
910
+ // that first so a huge direct-call string is never encoded in full.
911
+ if (code.length > EXECUTE_MAX_CODE_BYTES ||
912
+ new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
913
+ const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
914
+ return Promise.resolve(failureResponse(message, {
915
+ code: { code: "invalid_args", message, retryable: false },
916
+ }));
917
+ }
918
+ return runEdge(Effect.suspend(() => {
919
+ const program = normalizeProgramSource(code);
920
+ const storage = config.resumable ? registry.resultsStorage() : undefined;
921
+ const runState = config.resumable && storage?.compareAndSet
922
+ ? RunState.fresh(storage, program, config.environment ?? freshEnvironment(), config.resumable, storageTimeoutMs)
923
+ : undefined;
924
+ return play(program, runState, diagnostics, options.signal);
925
+ }));
926
+ },
927
+ replay: (runState, signal) => play(runState.program, runState, false, signal),
928
+ };
929
+ }
930
+ /** The execute_code handler. Exported for direct testing. */
931
+ export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
932
+ return createProgramRunner(registry, baseUrl, executor, logger, activity, config).execute;
566
933
  }
567
934
  /** An execution that never produced an ExecuteResult, as the model sees it. */
568
935
  function failedRun(err, logger, { emitted, diagnostics }) {
@@ -750,26 +1117,27 @@ function connectorInventory(connectors) {
750
1117
  return `${prefix}${shown.join(", ")}.`;
751
1118
  return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
752
1119
  }
753
- const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors) => `Use the configured services below to answer the task. A known address uses call_tool. Unknown-address and wider read-only work uses one execute_code program for discovery, calls, and reduction. Do not return catalog matches alone. Only readOnlyHint: true tools are available. Limits: ${hostLimits.maxHostCalls} host calls, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
1120
+ const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, resumable) => `Use the configured services below to answer the task. A known address uses call_tool. Unknown-address and wider ${resumable ? "work, writes included," : "read-only work"} uses one execute_code program for discovery, calls, and reduction. Do not return catalog matches alone. ${resumable
1121
+ ? `Writes pause for resume_execution. Keep an authorized batch in one program; approval "tool" covers later same-address calls, other approval-required writes pause separately. Limits: ${hostLimits.maxHostCalls} host calls, ${resumable.maxWrites} writes`
1122
+ : `Only readOnlyHint: true tools are available. Limits: ${hostLimits.maxHostCalls} host calls`}, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
754
1123
 
755
1124
  ${connectorInventory(connectors)}
756
1125
 
757
- Read relevant guides using top-level skills, not sandbox code. Write a plain-JavaScript async arrow:
758
- - connecta.search({ connector, query, safety: "readOnly", includeSchemas: "json" }) returns { tools }. Search each operation separately; choose by connectorTitle and schemas. Use schema.required and .properties to build args, never guessed fields. Compact schemas are text.
759
- - connecta.describe({ address }) returns { tools } for unclear schemas.
1126
+ Read guides with top-level skills. Write async () => { ... } using the global connecta:
1127
+ - connecta.search({ connector, query, ${resumable ? "" : 'safety: "readOnly", '}includeSchemas: "json" }) returns { tools }. Search operations separately; choose by connectorTitle and schema.required/.properties, never guessed fields. Compact schemas are text.
1128
+ - connecta.describe({ address }) clarifies schemas.
760
1129
  - connecta.call(address, args) returns the provider value directly.
761
- - Use Promise.all for independent calls, or Promise.allSettled to retain failures. Check status before reading value; rejected calls and missing fields are unknown, never false or zero.
762
- - connecta.emit(block): { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }; success-only, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes.
763
- - console.log(...) is captured. Return data for the client to render.
1130
+ - Use Promise.all for independent calls, or Promise.allSettled to retain failures. Check status; missing values are unknown, never false or zero.
1131
+ - connecta.emit(block): { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }; success-only, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. console.log(...) is captured.
764
1132
 
765
- No portable ambient capabilities. Return reduced JSON. If a provider result has an unfamiliar shape, return a small sample and continue in another call; never guess fields or use the whole text as an id. Top-level skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}; skills({ name: "investigate" }): task planning.`;
1133
+ No portable ambient capabilities. Return reduced JSON. Sample unfamiliar reads. Reduce a one-time write's full result here, or use a direct call and get_result paging; never repeat it to recover output. Never guess fields or use the whole text as an id. Top-level skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}; skills({ name: "investigate" }): task planning.`;
766
1134
  // Module scope, like the other six meta-tool inputs: its JSON Schema is
767
1135
  // derived once per process. Budgets and connectors vary by deployment and
768
1136
  // view, so they live in the description, never here.
769
1137
  const EXECUTE_INPUT = advertisedSchema(z.object({
770
1138
  code: z
771
1139
  .string()
772
- .describe("One complete JavaScript async arrow function that discovers, calls, and returns the reduced answer."),
1140
+ .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."),
773
1141
  diagnostics: z
774
1142
  .boolean()
775
1143
  .optional()
@@ -790,7 +1158,7 @@ export function registerExecuteTool(server, registry, ctx) {
790
1158
  hostCallTimeoutMs: resolveBudget(ctx.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS),
791
1159
  };
792
1160
  const connectors = registry.listConnectors();
793
- const handler = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
1161
+ const runner = createProgramRunner(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
794
1162
  discoveryConcurrency: ctx.discoveryConcurrency,
795
1163
  probeTimeoutMs: ctx.probeTimeoutMs,
796
1164
  maxEmittedBytes: emitBudgets.maxBytes,
@@ -799,38 +1167,26 @@ export function registerExecuteTool(server, registry, ctx) {
799
1167
  hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
800
1168
  watchdogMs: ctx.watchdogMs,
801
1169
  defer: ctx.defer,
1170
+ resumable: ctx.resumable,
1171
+ approval: ctx.approval,
1172
+ maxWrites: ctx.maxWrites,
802
1173
  });
803
1174
  server.registerTool("execute_code", {
804
- description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors),
1175
+ description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors, ctx.resumable),
805
1176
  inputSchema: EXECUTE_INPUT,
806
- // This hint describes connector calls, all explicitly read-only. The
807
- // supported executor constructions deny outbound access, filesystem,
808
- // and deployment config; X5 documents Dynamic runtime modules separately.
1177
+ // This hint describes connector calls: only explicitly read-only ones
1178
+ // run here, and any other call pauses the run unsent (W1) — the write
1179
+ // itself runs inside resume_execution, which is annotated destructive.
1180
+ // The supported executor constructions deny outbound access,
1181
+ // filesystem, and deployment config; X5 documents Dynamic runtime
1182
+ // modules separately.
809
1183
  annotations: {
810
1184
  readOnlyHint: true,
811
1185
  destructiveHint: false,
812
1186
  openWorldHint: true,
813
1187
  },
814
- }, async (args, extra) => {
815
- const controller = new AbortController();
816
- const signals = [extra.mcpReq.signal, ctx.requestSignal].filter((signal) => signal !== undefined);
817
- const forwarders = signals.map((signal) => {
818
- const forward = () => controller.abort(signal.reason);
819
- if (signal.aborted)
820
- forward();
821
- else
822
- signal.addEventListener("abort", forward, { once: true });
823
- return { signal, forward };
824
- });
825
- try {
826
- return await handler(args, {
827
- signal: controller.signal,
828
- });
829
- }
830
- finally {
831
- for (const { signal, forward } of forwarders) {
832
- signal.removeEventListener("abort", forward);
833
- }
834
- }
835
- });
1188
+ }, (args, extra) => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => runner.execute(args, {
1189
+ signal,
1190
+ })));
1191
+ return runner;
836
1192
  }