@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.
- package/AGENTS.md +6 -5
- package/CHANGELOG.md +387 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +80 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +478 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +217 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +653 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +397 -48
- package/dist/auth/oauth-start-reset.js +20 -0
- package/dist/branding.d.ts +87 -0
- package/dist/branding.js +170 -1
- package/dist/catalog-service.js +39 -29
- package/dist/catalog.js +42 -8
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connectors/remote-mcp.js +48 -9
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +431 -75
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +79 -14
- package/dist/executors/quickjs.js +4 -2
- package/dist/index.d.ts +62 -6
- package/dist/index.js +230 -6
- package/dist/invocation.js +56 -10
- package/dist/meta-tools.js +7 -2
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +62 -5
- package/dist/operator-ui/view.js +372 -28
- package/dist/page-styles.js +7 -0
- package/dist/program-source.js +116 -0
- package/dist/registry.d.ts +42 -6
- package/dist/registry.js +164 -60
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +6 -69
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +27 -5
- package/dist/routes/mcp.js +120 -59
- package/dist/routes/oauth-management.js +87 -20
- package/dist/routes/oauth.js +54 -87
- package/dist/routes/operator.js +1 -0
- package/dist/routes/shared.d.ts +23 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +2 -1
- package/dist/routes/ui.js +19 -4
- package/dist/run-journal.js +347 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/server.js +30 -2
- package/dist/skills.js +30 -11
- package/dist/timeout.js +26 -0
- package/dist/tool-safety.d.ts +35 -0
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +23 -0
- package/dist/ui.d.ts +17 -5
- package/dist/ui.js +51 -68
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +214 -27
- package/documentation/auth.md +186 -2
- package/documentation/code-mode.md +236 -63
- package/documentation/meta-tools.md +55 -29
- package/ethos.md +12 -10
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +174 -10
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/index.ts +36 -1
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +5 -1
- package/package.json +6 -2
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- 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 {
|
|
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
|
|
316
|
-
|
|
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) =>
|
|
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
|
-
?
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
498
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
},
|
|
558
|
-
|
|
559
|
-
|
|
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.
|
|
563
|
-
|
|
564
|
-
|
|
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.
|
|
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
|
|
758
|
-
- connecta.search({ connector, query, safety: "readOnly", includeSchemas: "json" }) returns { tools }. Search
|
|
759
|
-
- connecta.describe({ address })
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
807
|
-
//
|
|
808
|
-
//
|
|
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
|
-
},
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
}
|