@zackbart/connecta 0.24.4 → 0.26.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.
- package/AGENTS.md +43 -12
- package/CHANGELOG.md +524 -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/advertised-schema.js +53 -0
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -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 +449 -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 +194 -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 +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +152 -106
- package/dist/catalog.js +297 -38
- package/dist/concurrency.js +24 -23
- 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/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +330 -240
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +745 -282
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +145 -63
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +255 -212
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +281 -26
- package/dist/invocation.js +371 -286
- package/dist/meta-tools.js +213 -94
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +103 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +128 -3
- package/dist/program-source.js +321 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +43 -7
- package/dist/registry.js +434 -288
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +74 -154
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +196 -163
- package/dist/routes/mcp.js +228 -112
- package/dist/routes/oauth-management.js +73 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +83 -0
- package/dist/routes/shared.d.ts +25 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +211 -0
- package/dist/routes/ui.js +76 -35
- package/dist/run-journal.js +347 -0
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +137 -89
- package/dist/skills.js +28 -9
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +39 -43
- package/dist/tool-safety.d.ts +27 -1
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +45 -7
- package/dist/ui.d.ts +25 -4
- package/dist/ui.js +77 -167
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +367 -16
- package/documentation/auth.md +163 -2
- package/documentation/code-mode.md +274 -56
- package/documentation/meta-tools.md +200 -44
- package/ethos.md +46 -47
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +224 -14
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +42 -7
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +21 -5
- package/package.json +16 -4
- 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/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
package/dist/execute.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
+
import { Cause, Deferred, Duration, Effect, Exit } from "effect";
|
|
1
2
|
import { z } from "zod";
|
|
3
|
+
import { advertisedSchema } from "./advertised-schema.js";
|
|
2
4
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
3
5
|
import { errorResult, jsonResult } from "./meta-tools.js";
|
|
4
6
|
import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
|
|
5
7
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
|
|
6
8
|
import { boundedEchoText, msg } from "./errors.js";
|
|
7
|
-
import { InvocationFailure, InvocationService, } from "./invocation.js";
|
|
9
|
+
import { InvocationFailure, InvocationService, timed, } from "./invocation.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";
|
|
15
|
+
import { fromSignal, runEdge } from "./runtime/run.js";
|
|
8
16
|
import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, } from "./skills.js";
|
|
9
17
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
10
18
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
11
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;
|
|
22
|
+
/**
|
|
23
|
+
* The outer ceiling on one execution. It sits above both executors' own
|
|
24
|
+
* deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
|
|
25
|
+
* always reports its own timeout first and this fires only for one that
|
|
26
|
+
* never settles at all.
|
|
27
|
+
*/
|
|
28
|
+
const EXECUTE_WATCHDOG_MS = 120_000;
|
|
12
29
|
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
13
30
|
export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
14
31
|
/**
|
|
@@ -261,11 +278,147 @@ function guestErrorPrelude(failureSecret) {
|
|
|
261
278
|
});
|
|
262
279
|
})(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
|
|
263
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
|
+
}
|
|
264
404
|
/**
|
|
265
405
|
* Expose one host provider and typed guest errors. Catalogs load only when
|
|
266
406
|
* the program calls a tool or asks search/describe.
|
|
267
407
|
*/
|
|
268
408
|
export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
|
|
409
|
+
return [sandboxProvider(registry, baseUrl, activity, limits)];
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* The `connecta` provider for one execution.
|
|
413
|
+
*
|
|
414
|
+
* Building it is synchronous and loads nothing. Each function is a Promise
|
|
415
|
+
* edge the executor awaits, and behind it one host call is one fiber: spend
|
|
416
|
+
* the budget, do the operation, and turn a typed failure into the frame the
|
|
417
|
+
* prelude rebuilds inside the guest. Nothing is shared between those fibers
|
|
418
|
+
* but the budget counter and this request's catalog, so a call the program
|
|
419
|
+
* never awaits cannot disturb the others.
|
|
420
|
+
*/
|
|
421
|
+
function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
269
422
|
// All host calls made by one execute_code invocation share a downstream
|
|
270
423
|
// connection, while a later invocation receives a fresh request scope.
|
|
271
424
|
const requestScope = {};
|
|
@@ -276,76 +429,208 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
276
429
|
concurrency: limits.discoveryConcurrency,
|
|
277
430
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
278
431
|
defer: limits.defer,
|
|
432
|
+
approval: limits.approval,
|
|
279
433
|
});
|
|
280
434
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
281
435
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
282
436
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
283
437
|
const failureSecret = guestFailureSecret();
|
|
438
|
+
const { signal, diagnostics } = limits;
|
|
284
439
|
let hostCalls = 0;
|
|
285
|
-
// L4/M7: discovery and invocation spend the same budget; emit
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
440
|
+
// L4/M7: discovery and invocation spend the same budget, on entry; emit
|
|
441
|
+
// does not.
|
|
442
|
+
const spendHostCall = Effect.suspend(() => ++hostCalls > maxHostCalls
|
|
443
|
+
? Effect.fail(guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`))
|
|
444
|
+
: Effect.void);
|
|
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",
|
|
294
489
|
timeoutMs: hostCallTimeoutMs,
|
|
295
|
-
...(
|
|
490
|
+
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
296
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)),
|
|
297
503
|
});
|
|
298
504
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
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.
|
|
301
511
|
*/
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
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));
|
|
305
523
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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));
|
|
531
|
+
});
|
|
532
|
+
// A call brings its own cancellation: the invocation pipeline reads the
|
|
533
|
+
// run's signal, refuses a call that starts after it, and records the
|
|
534
|
+
// cancelled attempt in activity like any other 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"))));
|
|
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 });
|
|
309
562
|
}
|
|
310
|
-
throw err;
|
|
311
563
|
}
|
|
312
|
-
|
|
313
|
-
|
|
564
|
+
return outcome.ok
|
|
565
|
+
? outcome.value
|
|
566
|
+
: yield* Effect.fail(new InvocationFailure(outcome.error));
|
|
567
|
+
});
|
|
568
|
+
// Discovery gets the same treatment from here (L2): once the run has
|
|
569
|
+
// ended no search or describe starts, and one still in flight fails
|
|
570
|
+
// `cancelled` instead of holding the program until its probe deadline.
|
|
571
|
+
// Policy failures use the same thrown vocabulary as calls and utilities;
|
|
572
|
+
// the transport below reconstructs their code inside the guest.
|
|
573
|
+
const discovery = (operation, seq, raw, read) => Effect.suspend(() => {
|
|
314
574
|
const started = Date.now();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
575
|
+
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
576
|
+
const reading = Effect.tryPromise({
|
|
577
|
+
try: read,
|
|
578
|
+
catch: (err) => err instanceof DiscoveryPolicyError
|
|
579
|
+
? guestFailure(err.code, err.message)
|
|
580
|
+
: err,
|
|
581
|
+
});
|
|
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))));
|
|
616
|
+
});
|
|
617
|
+
const operations = {
|
|
618
|
+
call,
|
|
336
619
|
// Emission is a provider function, never an ExecuteResult field —
|
|
337
620
|
// that is what keeps the Executor contract untouched and parity
|
|
338
621
|
// structural (M8). It spends no host-call budget (M7); its own
|
|
339
|
-
// budgets live in the collector.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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({
|
|
625
|
+
try: () => {
|
|
626
|
+
if (!limits.emitCollector) {
|
|
627
|
+
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
628
|
+
}
|
|
629
|
+
limits.emitCollector.accept(block);
|
|
630
|
+
},
|
|
631
|
+
catch: (err) => err,
|
|
632
|
+
}),
|
|
633
|
+
search: (seq, raw) => discovery("search", seq, raw, async () => {
|
|
349
634
|
const args = (raw ?? {});
|
|
350
635
|
const result = flatSearchResult(await catalog.search({
|
|
351
636
|
...args,
|
|
@@ -353,230 +638,413 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
353
638
|
}));
|
|
354
639
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
355
640
|
return result;
|
|
356
|
-
})
|
|
357
|
-
describe: (raw) =>
|
|
641
|
+
}),
|
|
642
|
+
describe: (seq, raw) => discovery("describe", seq, raw, async () => {
|
|
358
643
|
const args = (raw ?? {});
|
|
359
644
|
const result = { tools: await catalog.describe(args) };
|
|
360
645
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
361
646
|
return result;
|
|
362
|
-
})
|
|
647
|
+
}),
|
|
363
648
|
};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
649
|
+
// Every failure leaves through the same frame, so the prelude can rebuild
|
|
650
|
+
// a typed error however the host call failed — refused, over budget, or
|
|
651
|
+
// cancelled. Anything that is not an InvocationFailure crosses unchanged.
|
|
652
|
+
const framed = (err) => Effect.suspend(() => {
|
|
653
|
+
if (!(err instanceof InvocationFailure))
|
|
654
|
+
return Effect.fail(err);
|
|
655
|
+
const failure = boundedGuestFailure(err);
|
|
656
|
+
const frame = framedGuestFailure(failureSecret, failure);
|
|
657
|
+
limits.onInvocationFailure?.(failure);
|
|
658
|
+
return Effect.fail(frame);
|
|
659
|
+
});
|
|
660
|
+
return {
|
|
661
|
+
name: "connecta",
|
|
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())}`,
|
|
665
|
+
fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
|
|
666
|
+
name,
|
|
667
|
+
(...args) => {
|
|
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);
|
|
678
|
+
// The executor owns this promise, and a program may abandon a call
|
|
679
|
+
// that the run's end then cancels before anyone has awaited it.
|
|
680
|
+
// The rejection is still there for whoever does; it is just not an
|
|
681
|
+
// unhandled rejection of the host's, which workerd reports as soon
|
|
682
|
+
// as a microtask passes without a handler.
|
|
683
|
+
settled.catch(() => { });
|
|
684
|
+
return settled;
|
|
685
|
+
},
|
|
686
|
+
])),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Await an executor's promise without trusting it to settle.
|
|
691
|
+
*
|
|
692
|
+
* An executor's deadline is its own business, and the Dynamic Worker's lives
|
|
693
|
+
* inside the sandbox: a wedged isolate never fires it, so the call never
|
|
694
|
+
* settles, the handler never returns, and the admission lease is held for
|
|
695
|
+
* good. Two of those fill the default code pool and every later execute_code
|
|
696
|
+
* queues behind them (executor hit this in production). An `acquire()` that
|
|
697
|
+
* ignores its signal holds a cancelled request the same way.
|
|
698
|
+
*
|
|
699
|
+
* So the host races the promise against the request's signal and, for a run,
|
|
700
|
+
* a ceiling of its own. Cancellation settles as the same `executor_cancelled`
|
|
701
|
+
* the QuickJS pool reports, unless the executor reports its own first; the
|
|
702
|
+
* ceiling as an untyped executor failure naming the key that sets it. Either
|
|
703
|
+
* way the handler returns and releases the lease, and the abandoned call
|
|
704
|
+
* keeps whatever it was doing: the QuickJS lease release recycles its child,
|
|
705
|
+
* a Dynamic Worker isolate runs on to its own deadline, and a lease granted
|
|
706
|
+
* after its request gave up goes to `late`. The result contract is untouched
|
|
707
|
+
* — this only decides when to stop waiting for one.
|
|
708
|
+
*/
|
|
709
|
+
function awaitExecutor(start, signal, options = {}) {
|
|
710
|
+
const contenders = [
|
|
711
|
+
Effect.suspend(() => {
|
|
712
|
+
let pending;
|
|
713
|
+
const settling = Effect.tryPromise({
|
|
714
|
+
try: () => (pending = Promise.resolve(start())),
|
|
715
|
+
catch: (err) => err,
|
|
716
|
+
});
|
|
717
|
+
const { late } = options;
|
|
718
|
+
return late
|
|
719
|
+
? settling.pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
720
|
+
pending?.then(late, () => { });
|
|
721
|
+
})))
|
|
722
|
+
: settling;
|
|
723
|
+
}),
|
|
724
|
+
// An executor that honors the signal rejects in the abort event itself,
|
|
725
|
+
// with the logs it captured attached, and that report beats ours by one
|
|
726
|
+
// timer turn. (A 1ms sleep, not 0: Effect.sleep(0) waits a microtask,
|
|
727
|
+
// which the executor's promise chain can still lose to.)
|
|
728
|
+
fromSignal(signal).pipe(Effect.catch(() => Effect.sleep(Duration.millis(1))), Effect.andThen(Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled.")))),
|
|
387
729
|
];
|
|
730
|
+
const { watchdog } = options;
|
|
731
|
+
if (watchdog) {
|
|
732
|
+
contenders.push(Effect.sleep(Duration.millis(watchdog.ms)).pipe(Effect.andThen(Effect.suspend(() => {
|
|
733
|
+
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", { watchdogMs: watchdog.ms });
|
|
734
|
+
return Effect.fail(new Error(`sandbox unresponsive: no outcome within the ${watchdog.ms}ms ` +
|
|
735
|
+
"execute.watchdogMs ceiling, so the run was abandoned"));
|
|
736
|
+
}))));
|
|
737
|
+
}
|
|
738
|
+
return Effect.raceAllFirst(contenders);
|
|
388
739
|
}
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
|
|
740
|
+
/**
|
|
741
|
+
* The run's own signal, aborted however the run ends. It follows the
|
|
742
|
+
* caller's, and aborting it on the way out is what releases outstanding host
|
|
743
|
+
* waits and tells cooperative connectors to stop.
|
|
744
|
+
*/
|
|
745
|
+
function runSignal(caller) {
|
|
746
|
+
return Effect.acquireRelease(Effect.sync(() => {
|
|
392
747
|
const controller = new AbortController();
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
else
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
748
|
+
const forward = () => controller.abort(caller?.reason);
|
|
749
|
+
if (caller?.aborted)
|
|
750
|
+
forward();
|
|
751
|
+
else
|
|
752
|
+
caller?.addEventListener("abort", forward, { once: true });
|
|
753
|
+
return { controller, forward };
|
|
754
|
+
}), ({ controller, forward }) => Effect.sync(() => {
|
|
755
|
+
controller.abort();
|
|
756
|
+
caller?.removeEventListener("abort", forward);
|
|
757
|
+
})).pipe(Effect.map(({ controller }) => controller.signal));
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* An admitting executor's lease, released when the run's scope closes.
|
|
761
|
+
*
|
|
762
|
+
* `acquire()` is the executor's Promise, awaited as one on purpose. A queued
|
|
763
|
+
* request is handed its lease from inside whichever request released one, so
|
|
764
|
+
* a fiber resumed straight from the executor's admission queue would build
|
|
765
|
+
* this request's providers and run its program in the releasing request —
|
|
766
|
+
* which workerd refuses for any I/O. Awaiting the promise resumes it here.
|
|
767
|
+
*/
|
|
768
|
+
function leased(executor, signal) {
|
|
769
|
+
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal }), signal, {
|
|
770
|
+
late: (lease) => lease.release(),
|
|
771
|
+
}), (lease) => Effect.sync(() => lease.release()));
|
|
772
|
+
}
|
|
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 = {}) {
|
|
782
|
+
const watchdog = {
|
|
783
|
+
ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
|
|
784
|
+
logger,
|
|
785
|
+
};
|
|
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(() => {
|
|
795
|
+
const diagnostics = diagnosticsRequested
|
|
796
|
+
? new ExecuteDiagnostics()
|
|
797
|
+
: undefined;
|
|
402
798
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
403
799
|
const invocationFailures = [];
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
800
|
+
const exemptWrites = runState ? undefined : new ExemptWrites();
|
|
801
|
+
// The run's scope holds its signal and its lease. However the run
|
|
802
|
+
// ends — a result, a thrown executor, the watchdog, cancellation —
|
|
803
|
+
// closing it releases the lease and then aborts the signal, so
|
|
804
|
+
// nothing the run started outlives the request.
|
|
805
|
+
const run = Effect.gen(function* () {
|
|
806
|
+
const signal = yield* runSignal(callerSignal);
|
|
807
|
+
// Admission comes before provider construction: queued calls retain
|
|
808
|
+
// no catalogs, request scopes, or provider closures.
|
|
809
|
+
let lease;
|
|
407
810
|
if (isAdmittingExecutor(executor)) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
finally {
|
|
413
|
-
if (diagnostics) {
|
|
414
|
-
diagnostics.admissionMs = Date.now() - admissionStarted;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
811
|
+
lease = yield* timed((elapsed) => {
|
|
812
|
+
if (diagnostics)
|
|
813
|
+
diagnostics.admissionMs = elapsed;
|
|
814
|
+
}, leased(executor, signal));
|
|
417
815
|
if ((lease.waitMs ?? 0) > 0) {
|
|
418
816
|
logger.debug("[connecta] execute_code admitted after queue wait", {
|
|
419
817
|
waitMs: lease.waitMs,
|
|
420
818
|
});
|
|
421
819
|
}
|
|
422
820
|
}
|
|
423
|
-
const
|
|
424
|
-
let providers;
|
|
425
|
-
try {
|
|
426
|
-
providers = await buildSandboxProviders(registry, baseUrl, logger, activity, {
|
|
427
|
-
signal: controller.signal,
|
|
428
|
-
onInvocationFailure: (failure) => {
|
|
429
|
-
invocationFailures.push(failure);
|
|
430
|
-
if (invocationFailures.length > 64)
|
|
431
|
-
invocationFailures.shift();
|
|
432
|
-
},
|
|
433
|
-
emitCollector: emitted,
|
|
434
|
-
...(diagnostics ? { diagnostics } : {}),
|
|
435
|
-
discoveryConcurrency: config.discoveryConcurrency,
|
|
436
|
-
probeTimeoutMs: config.probeTimeoutMs,
|
|
437
|
-
maxHostCalls: config.maxHostCalls,
|
|
438
|
-
hostCallTimeoutMs: config.hostCallTimeoutMs,
|
|
439
|
-
defer: config.defer,
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
finally {
|
|
821
|
+
const provider = yield* timed((elapsed) => {
|
|
443
822
|
if (diagnostics)
|
|
444
|
-
diagnostics.setupMs =
|
|
823
|
+
diagnostics.setupMs = elapsed;
|
|
824
|
+
}, Effect.sync(() => sandboxProvider(registry, baseUrl, activity, {
|
|
825
|
+
signal,
|
|
826
|
+
onInvocationFailure: (failure) => {
|
|
827
|
+
invocationFailures.push(failure);
|
|
828
|
+
if (invocationFailures.length > 64)
|
|
829
|
+
invocationFailures.shift();
|
|
830
|
+
},
|
|
831
|
+
emitCollector: emitted,
|
|
832
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
833
|
+
discoveryConcurrency: config.discoveryConcurrency,
|
|
834
|
+
probeTimeoutMs: config.probeTimeoutMs,
|
|
835
|
+
maxHostCalls: config.maxHostCalls,
|
|
836
|
+
hostCallTimeoutMs,
|
|
837
|
+
defer: config.defer,
|
|
838
|
+
environment: config.environment,
|
|
839
|
+
runState,
|
|
840
|
+
approval: config.approval,
|
|
841
|
+
maxWrites: config.maxWrites,
|
|
842
|
+
exemptWrites,
|
|
843
|
+
})));
|
|
844
|
+
if (signal.aborted) {
|
|
845
|
+
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
445
846
|
}
|
|
446
|
-
|
|
447
|
-
|
|
847
|
+
const admitted = lease;
|
|
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
|
+
}))));
|
|
448
861
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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) => {
|
|
869
|
+
if (diagnostics)
|
|
870
|
+
diagnostics.executorWallMs = elapsed;
|
|
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;
|
|
878
|
+
});
|
|
879
|
+
const reported = { emitted, diagnostics, invocationFailures };
|
|
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);
|
|
454
886
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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"));
|
|
459
892
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (err.code === "executor_overloaded") {
|
|
467
|
-
logger.warn("[connecta] execute_code admission rejected", {
|
|
468
|
-
code: err.code,
|
|
469
|
-
retryAfterMs: err.retryAfterMs,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
return failureResponse(err.message, {
|
|
473
|
-
logs,
|
|
474
|
-
emitted: err instanceof ExecutorExecutionError ? emitted : undefined,
|
|
475
|
-
diagnostics,
|
|
476
|
-
code: {
|
|
477
|
-
code: err.code,
|
|
478
|
-
message: err.message,
|
|
479
|
-
retryable: err.retryable,
|
|
480
|
-
...(err.retryAfterMs !== undefined
|
|
481
|
-
? { retryAfterMs: err.retryAfterMs }
|
|
482
|
-
: {}),
|
|
483
|
-
},
|
|
484
|
-
});
|
|
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
|
+
}));
|
|
485
899
|
}
|
|
486
|
-
return
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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;
|
|
933
|
+
}
|
|
934
|
+
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
|
935
|
+
function failedRun(err, logger, { emitted, diagnostics }) {
|
|
936
|
+
const logs = err !== null && typeof err === "object" && "logs" in err
|
|
937
|
+
? executeLogs(err.logs)
|
|
938
|
+
: undefined;
|
|
939
|
+
if (err instanceof ExecutorAdmissionError) {
|
|
940
|
+
if (err.code === "executor_overloaded") {
|
|
941
|
+
logger.warn("[connecta] execute_code admission rejected", {
|
|
942
|
+
code: err.code,
|
|
943
|
+
retryAfterMs: err.retryAfterMs,
|
|
491
944
|
});
|
|
492
945
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
946
|
+
return failureResponse(err.message, {
|
|
947
|
+
logs,
|
|
948
|
+
emitted: err instanceof ExecutorExecutionError ? emitted : undefined,
|
|
949
|
+
diagnostics,
|
|
950
|
+
code: {
|
|
951
|
+
code: err.code,
|
|
952
|
+
message: err.message,
|
|
953
|
+
retryable: err.retryable,
|
|
954
|
+
...(err.retryAfterMs !== undefined
|
|
955
|
+
? { retryAfterMs: err.retryAfterMs }
|
|
956
|
+
: {}),
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
return failureResponse(`Executor failed: ${msg(err)}`, {
|
|
961
|
+
logs,
|
|
962
|
+
emitted,
|
|
963
|
+
diagnostics,
|
|
964
|
+
code: "executor_failed",
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/** The response for an ExecuteResult: the program's error or its value. */
|
|
968
|
+
function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
|
|
969
|
+
const logs = executeLogs(outcome.logs);
|
|
970
|
+
if (outcome.error !== undefined) {
|
|
971
|
+
// Executor bridges necessarily reduce thrown host errors to strings.
|
|
972
|
+
// Match that terminal string back to the request-local typed failure so
|
|
973
|
+
// an unhandled tool failure keeps the same structured contract as
|
|
974
|
+
// call_tool. Failures caught by model code never reach
|
|
975
|
+
// outcome.error and therefore remain under that code's control.
|
|
976
|
+
//
|
|
977
|
+
// An error the program let through unchanged matches exactly, and an
|
|
978
|
+
// exact match always wins: a program that wrapped one failure's message
|
|
979
|
+
// around another's must not have the wrong type attached. Containment is
|
|
980
|
+
// the fallback, so a wrapped message still reports its underlying type
|
|
981
|
+
// rather than losing it to prose.
|
|
982
|
+
let invocationFailure;
|
|
983
|
+
for (const match of [
|
|
984
|
+
(candidate) => outcome.error !== "" &&
|
|
985
|
+
[candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
|
|
986
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) =>
|
|
987
|
+
// E6: empty or tiny prose cannot identify a wrapped failure.
|
|
988
|
+
message !== undefined &&
|
|
989
|
+
message.length >= 8 &&
|
|
990
|
+
outcome.error?.includes(message) === true),
|
|
991
|
+
]) {
|
|
992
|
+
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
993
|
+
const candidate = invocationFailures[i];
|
|
994
|
+
if (candidate && match(candidate)) {
|
|
995
|
+
invocationFailure = candidate;
|
|
531
996
|
break;
|
|
997
|
+
}
|
|
532
998
|
}
|
|
533
|
-
if (invocationFailure)
|
|
534
|
-
|
|
535
|
-
return failureResponse(invocationFailure.details.message, {
|
|
536
|
-
logs,
|
|
537
|
-
emitted,
|
|
538
|
-
diagnostics,
|
|
539
|
-
code: invocationFailure.details,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
|
|
543
|
-
return failureResponse(message, {
|
|
544
|
-
logs,
|
|
545
|
-
emitted,
|
|
546
|
-
diagnostics,
|
|
547
|
-
code: "executor_failed",
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
551
|
-
// value) makes JSON.stringify throw — keep that inside the structured
|
|
552
|
-
// error path so captured logs survive instead of a raw SDK 500.
|
|
553
|
-
let result;
|
|
554
|
-
try {
|
|
555
|
-
result = guardExecuteResultValue(outcome.result);
|
|
999
|
+
if (invocationFailure)
|
|
1000
|
+
break;
|
|
556
1001
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return failureResponse(message, {
|
|
1002
|
+
if (invocationFailure) {
|
|
1003
|
+
// E1/X11: return the same bounded details the guest received.
|
|
1004
|
+
return failureResponse(invocationFailure.details.message, {
|
|
560
1005
|
logs,
|
|
561
1006
|
emitted,
|
|
562
1007
|
diagnostics,
|
|
563
|
-
code:
|
|
1008
|
+
code: invocationFailure.details,
|
|
564
1009
|
});
|
|
565
1010
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1011
|
+
const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
|
|
1012
|
+
return failureResponse(message, {
|
|
1013
|
+
logs,
|
|
1014
|
+
emitted,
|
|
1015
|
+
diagnostics,
|
|
1016
|
+
code: "executor_failed",
|
|
571
1017
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
1018
|
+
}
|
|
1019
|
+
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
1020
|
+
// value) makes JSON.stringify throw — keep that inside the structured
|
|
1021
|
+
// error path so captured logs survive instead of a raw SDK 500.
|
|
1022
|
+
let result;
|
|
1023
|
+
try {
|
|
1024
|
+
result = guardExecuteResultValue(outcome.result);
|
|
1025
|
+
}
|
|
1026
|
+
catch (err) {
|
|
1027
|
+
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
1028
|
+
return failureResponse(message, {
|
|
1029
|
+
logs,
|
|
1030
|
+
emitted,
|
|
1031
|
+
diagnostics,
|
|
1032
|
+
code: "executor_failed",
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
const response = jsonResult({
|
|
1036
|
+
result,
|
|
1037
|
+
...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
|
|
1038
|
+
...(logs ? { logs } : {}),
|
|
1039
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
1040
|
+
});
|
|
1041
|
+
if (emitted.blocks.length > 0) {
|
|
1042
|
+
// Emitted image/audio blocks are valid MCP content that ToolResult's
|
|
1043
|
+
// text-only typing does not model — the same acknowledged gap
|
|
1044
|
+
// guardContent lives with for downstream block passthrough.
|
|
1045
|
+
response.content.push(...emitted.blocks);
|
|
1046
|
+
}
|
|
1047
|
+
return response;
|
|
580
1048
|
}
|
|
581
1049
|
function executeLogs(value) {
|
|
582
1050
|
return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
|
|
@@ -649,12 +1117,14 @@ function connectorInventory(connectors) {
|
|
|
649
1117
|
return `${prefix}${shown.join(", ")}.`;
|
|
650
1118
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
651
1119
|
}
|
|
652
|
-
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. 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.
|
|
653
1123
|
|
|
654
1124
|
${connectorInventory(connectors)}
|
|
655
1125
|
|
|
656
|
-
Read
|
|
657
|
-
- 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.
|
|
1126
|
+
Read guides with top-level skills. Write JavaScript as async () => { ... }; connecta is global, not a parameter:
|
|
1127
|
+
- connecta.search({ connector, query, ${resumable ? "" : '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.
|
|
658
1128
|
- connecta.describe({ address }) returns { tools } for unclear schemas.
|
|
659
1129
|
- connecta.call(address, args) returns the provider value directly.
|
|
660
1130
|
- 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.
|
|
@@ -662,6 +1132,18 @@ Read relevant guides using top-level skills, not sandbox code. Write a plain-Jav
|
|
|
662
1132
|
- console.log(...) is captured. Return data for the client to render.
|
|
663
1133
|
|
|
664
1134
|
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.`;
|
|
1135
|
+
// Module scope, like the other six meta-tool inputs: its JSON Schema is
|
|
1136
|
+
// derived once per process. Budgets and connectors vary by deployment and
|
|
1137
|
+
// view, so they live in the description, never here.
|
|
1138
|
+
const EXECUTE_INPUT = advertisedSchema(z.object({
|
|
1139
|
+
code: z
|
|
1140
|
+
.string()
|
|
1141
|
+
.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."),
|
|
1142
|
+
diagnostics: z
|
|
1143
|
+
.boolean()
|
|
1144
|
+
.optional()
|
|
1145
|
+
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
1146
|
+
}));
|
|
665
1147
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
666
1148
|
export function registerExecuteTool(server, registry, ctx) {
|
|
667
1149
|
// Resolved once so the description and the collector cannot disagree about
|
|
@@ -677,54 +1159,35 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
677
1159
|
hostCallTimeoutMs: resolveBudget(ctx.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS),
|
|
678
1160
|
};
|
|
679
1161
|
const connectors = registry.listConnectors();
|
|
680
|
-
const
|
|
1162
|
+
const runner = createProgramRunner(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
|
|
681
1163
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
682
1164
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
683
1165
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
684
1166
|
maxEmittedBlocks: emitBudgets.maxBlocks,
|
|
685
1167
|
maxHostCalls: hostLimits.maxHostCalls,
|
|
686
1168
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
1169
|
+
watchdogMs: ctx.watchdogMs,
|
|
687
1170
|
defer: ctx.defer,
|
|
1171
|
+
resumable: ctx.resumable,
|
|
1172
|
+
approval: ctx.approval,
|
|
1173
|
+
maxWrites: ctx.maxWrites,
|
|
688
1174
|
});
|
|
689
1175
|
server.registerTool("execute_code", {
|
|
690
|
-
description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors),
|
|
691
|
-
inputSchema:
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
699
|
-
}),
|
|
700
|
-
// This hint describes connector calls, all explicitly read-only. The
|
|
701
|
-
// supported executor constructions deny outbound access, filesystem,
|
|
702
|
-
// and deployment config; X5 documents Dynamic runtime modules separately.
|
|
1176
|
+
description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors, ctx.resumable),
|
|
1177
|
+
inputSchema: EXECUTE_INPUT,
|
|
1178
|
+
// This hint describes connector calls: only explicitly read-only ones
|
|
1179
|
+
// run here, and any other call pauses the run unsent (W1) — the write
|
|
1180
|
+
// itself runs inside resume_execution, which is annotated destructive.
|
|
1181
|
+
// The supported executor constructions deny outbound access,
|
|
1182
|
+
// filesystem, and deployment config; X5 documents Dynamic runtime
|
|
1183
|
+
// modules separately.
|
|
703
1184
|
annotations: {
|
|
704
1185
|
readOnlyHint: true,
|
|
705
1186
|
destructiveHint: false,
|
|
706
1187
|
openWorldHint: true,
|
|
707
1188
|
},
|
|
708
|
-
},
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const forward = () => controller.abort(signal.reason);
|
|
713
|
-
if (signal.aborted)
|
|
714
|
-
forward();
|
|
715
|
-
else
|
|
716
|
-
signal.addEventListener("abort", forward, { once: true });
|
|
717
|
-
return { signal, forward };
|
|
718
|
-
});
|
|
719
|
-
try {
|
|
720
|
-
return await handler(args, {
|
|
721
|
-
signal: controller.signal,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
finally {
|
|
725
|
-
for (const { signal, forward } of forwarders) {
|
|
726
|
-
signal.removeEventListener("abort", forward);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
});
|
|
1189
|
+
}, (args, extra) => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => runner.execute(args, {
|
|
1190
|
+
signal,
|
|
1191
|
+
})));
|
|
1192
|
+
return runner;
|
|
730
1193
|
}
|