@zackbart/connecta 0.24.4 → 0.25.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 +40 -10
- package/CHANGELOG.md +253 -0
- package/dist/advertised-schema.js +53 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +141 -105
- package/dist/catalog.js +258 -33
- package/dist/concurrency.js +24 -23
- 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 +317 -241
- 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/execute.js +363 -257
- package/dist/executor-admission.js +67 -50
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +251 -210
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +51 -20
- package/dist/invocation.js +324 -285
- package/dist/meta-tools.js +206 -92
- 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 +46 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +38 -2
- package/dist/program-source.js +205 -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 +1 -1
- package/dist/registry.js +305 -260
- package/dist/routes/activity.js +120 -137
- package/dist/routes/credentials.js +174 -163
- package/dist/routes/mcp.js +129 -70
- package/dist/routes/oauth-management.js +60 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +82 -0
- package/dist/routes/shared.d.ts +2 -0
- package/dist/routes/ui-data.js +210 -0
- package/dist/routes/ui.js +64 -32
- 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/storage.js +66 -0
- package/dist/server.js +111 -89
- package/dist/skills.js +1 -1
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +17 -47
- package/dist/types.d.ts +27 -7
- package/dist/ui.d.ts +16 -2
- package/dist/ui.js +50 -166
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +184 -10
- package/documentation/auth.md +80 -0
- package/documentation/code-mode.md +69 -24
- package/documentation/meta-tools.md +164 -27
- package/ethos.md +44 -47
- package/examples/worker/README.md +51 -5
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +6 -6
- package/examples/worker/wrangler.jsonc +16 -4
- package/package.json +11 -3
- package/templates/node/package.json +1 -1
- 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/tool-safety.d.ts +0 -9
package/dist/execute.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import { Cause, 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 { withoutProgramTerminator } from "./program-source.js";
|
|
11
|
+
import { fromSignal, runEdge } from "./runtime/run.js";
|
|
8
12
|
import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, } from "./skills.js";
|
|
9
13
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
10
14
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
11
15
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
|
|
16
|
+
/**
|
|
17
|
+
* The outer ceiling on one execution. It sits above both executors' own
|
|
18
|
+
* deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
|
|
19
|
+
* always reports its own timeout first and this fires only for one that
|
|
20
|
+
* never settles at all.
|
|
21
|
+
*/
|
|
22
|
+
const EXECUTE_WATCHDOG_MS = 120_000;
|
|
12
23
|
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
13
24
|
export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
14
25
|
/**
|
|
@@ -266,6 +277,19 @@ function guestErrorPrelude(failureSecret) {
|
|
|
266
277
|
* the program calls a tool or asks search/describe.
|
|
267
278
|
*/
|
|
268
279
|
export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
|
|
280
|
+
return [sandboxProvider(registry, baseUrl, activity, limits)];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The `connecta` provider for one execution.
|
|
284
|
+
*
|
|
285
|
+
* Building it is synchronous and loads nothing. Each function is a Promise
|
|
286
|
+
* edge the executor awaits, and behind it one host call is one fiber: spend
|
|
287
|
+
* the budget, do the operation, and turn a typed failure into the frame the
|
|
288
|
+
* prelude rebuilds inside the guest. Nothing is shared between those fibers
|
|
289
|
+
* but the budget counter and this request's catalog, so a call the program
|
|
290
|
+
* never awaits cannot disturb the others.
|
|
291
|
+
*/
|
|
292
|
+
function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
269
293
|
// All host calls made by one execute_code invocation share a downstream
|
|
270
294
|
// connection, while a later invocation receives a fresh request scope.
|
|
271
295
|
const requestScope = {};
|
|
@@ -281,71 +305,67 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
281
305
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
282
306
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
283
307
|
const failureSecret = guestFailureSecret();
|
|
308
|
+
const { signal, diagnostics } = limits;
|
|
284
309
|
let hostCalls = 0;
|
|
285
|
-
// L4/M7: discovery and invocation spend the same budget; emit
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
};
|
|
310
|
+
// L4/M7: discovery and invocation spend the same budget, on entry; emit
|
|
311
|
+
// does not.
|
|
312
|
+
const spendHostCall = Effect.suspend(() => ++hostCalls > maxHostCalls
|
|
313
|
+
? Effect.fail(guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`))
|
|
314
|
+
: Effect.void);
|
|
292
315
|
const invocationContext = () => ({
|
|
293
316
|
source: "execute_code",
|
|
294
317
|
timeoutMs: hostCallTimeoutMs,
|
|
295
|
-
...(
|
|
318
|
+
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
296
319
|
unwrapResult: true,
|
|
297
320
|
});
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const timedCatalog = async (operation, fn) => {
|
|
321
|
+
// A call brings its own cancellation: the invocation pipeline reads the
|
|
322
|
+
// run's signal, refuses a call that starts after it, and records the
|
|
323
|
+
// cancelled attempt in activity like any other outcome.
|
|
324
|
+
const call = (address, args) => spendHostCall.pipe(Effect.andThen(Effect.suspend(() => invocation.pipeline(String(address), args ?? {}, invocationContext()))), Effect.flatMap((outcome) => {
|
|
325
|
+
diagnostics?.recordCall(outcome);
|
|
326
|
+
return outcome.ok
|
|
327
|
+
? Effect.succeed(outcome.value)
|
|
328
|
+
: Effect.fail(new InvocationFailure(outcome.error));
|
|
329
|
+
}));
|
|
330
|
+
// Discovery gets the same treatment from here (L2): once the run has
|
|
331
|
+
// ended no search or describe starts, and one still in flight fails
|
|
332
|
+
// `cancelled` instead of holding the program until its probe deadline.
|
|
333
|
+
// Policy failures use the same thrown vocabulary as calls and utilities;
|
|
334
|
+
// the transport below reconstructs their code inside the guest.
|
|
335
|
+
const discovery = (operation, read) => Effect.suspend(() => {
|
|
314
336
|
const started = Date.now();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
};
|
|
334
|
-
const fns = {
|
|
335
|
-
call: (address, args) => callAddress(address, args),
|
|
337
|
+
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
338
|
+
const reading = Effect.tryPromise({
|
|
339
|
+
try: read,
|
|
340
|
+
catch: (err) => err instanceof DiscoveryPolicyError
|
|
341
|
+
? guestFailure(err.code, err.message)
|
|
342
|
+
: err,
|
|
343
|
+
});
|
|
344
|
+
return spendHostCall.pipe(Effect.andThen(!signal
|
|
345
|
+
? reading
|
|
346
|
+
: signal.aborted
|
|
347
|
+
? Effect.fail(cancelled())
|
|
348
|
+
: Effect.raceAllFirst([
|
|
349
|
+
reading,
|
|
350
|
+
fromSignal(signal).pipe(Effect.mapError(cancelled)),
|
|
351
|
+
])), Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
352
|
+
});
|
|
353
|
+
const operations = {
|
|
354
|
+
call,
|
|
336
355
|
// Emission is a provider function, never an ExecuteResult field —
|
|
337
356
|
// that is what keeps the Executor contract untouched and parity
|
|
338
357
|
// structural (M8). It spends no host-call budget (M7); its own
|
|
339
358
|
// budgets live in the collector.
|
|
340
|
-
emit:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
emit: (block) => Effect.try({
|
|
360
|
+
try: () => {
|
|
361
|
+
if (!limits.emitCollector) {
|
|
362
|
+
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
363
|
+
}
|
|
364
|
+
limits.emitCollector.accept(block);
|
|
365
|
+
},
|
|
366
|
+
catch: (err) => err,
|
|
367
|
+
}),
|
|
368
|
+
search: (raw) => discovery("search", async () => {
|
|
349
369
|
const args = (raw ?? {});
|
|
350
370
|
const result = flatSearchResult(await catalog.search({
|
|
351
371
|
...args,
|
|
@@ -353,230 +373,311 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
353
373
|
}));
|
|
354
374
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
355
375
|
return result;
|
|
356
|
-
})
|
|
357
|
-
describe: (raw) =>
|
|
376
|
+
}),
|
|
377
|
+
describe: (raw) => discovery("describe", async () => {
|
|
358
378
|
const args = (raw ?? {});
|
|
359
379
|
const result = { tools: await catalog.describe(args) };
|
|
360
380
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
361
381
|
return result;
|
|
362
|
-
})
|
|
382
|
+
}),
|
|
363
383
|
};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
384
|
+
// Every failure leaves through the same frame, so the prelude can rebuild
|
|
385
|
+
// a typed error however the host call failed — refused, over budget, or
|
|
386
|
+
// cancelled. Anything that is not an InvocationFailure crosses unchanged.
|
|
387
|
+
const framed = (err) => Effect.suspend(() => {
|
|
388
|
+
if (!(err instanceof InvocationFailure))
|
|
389
|
+
return Effect.fail(err);
|
|
390
|
+
const failure = boundedGuestFailure(err);
|
|
391
|
+
const frame = framedGuestFailure(failureSecret, failure);
|
|
392
|
+
limits.onInvocationFailure?.(failure);
|
|
393
|
+
return Effect.fail(frame);
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
name: "connecta",
|
|
397
|
+
prelude: guestErrorPrelude(failureSecret),
|
|
398
|
+
fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
|
|
399
|
+
name,
|
|
400
|
+
(...args) => {
|
|
401
|
+
const settled = runEdge(Effect.suspend(() => operation(...args)).pipe(Effect.catch(framed)));
|
|
402
|
+
// The executor owns this promise, and a program may abandon a call
|
|
403
|
+
// that the run's end then cancels before anyone has awaited it.
|
|
404
|
+
// The rejection is still there for whoever does; it is just not an
|
|
405
|
+
// unhandled rejection of the host's, which workerd reports as soon
|
|
406
|
+
// as a microtask passes without a handler.
|
|
407
|
+
settled.catch(() => { });
|
|
408
|
+
return settled;
|
|
409
|
+
},
|
|
410
|
+
])),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Await an executor's promise without trusting it to settle.
|
|
415
|
+
*
|
|
416
|
+
* An executor's deadline is its own business, and the Dynamic Worker's lives
|
|
417
|
+
* inside the sandbox: a wedged isolate never fires it, so the call never
|
|
418
|
+
* settles, the handler never returns, and the admission lease is held for
|
|
419
|
+
* good. Two of those fill the default code pool and every later execute_code
|
|
420
|
+
* queues behind them (executor hit this in production). An `acquire()` that
|
|
421
|
+
* ignores its signal holds a cancelled request the same way.
|
|
422
|
+
*
|
|
423
|
+
* So the host races the promise against the request's signal and, for a run,
|
|
424
|
+
* a ceiling of its own. Cancellation settles as the same `executor_cancelled`
|
|
425
|
+
* the QuickJS pool reports, unless the executor reports its own first; the
|
|
426
|
+
* ceiling as an untyped executor failure naming the key that sets it. Either
|
|
427
|
+
* way the handler returns and releases the lease, and the abandoned call
|
|
428
|
+
* keeps whatever it was doing: the QuickJS lease release recycles its child,
|
|
429
|
+
* a Dynamic Worker isolate runs on to its own deadline, and a lease granted
|
|
430
|
+
* after its request gave up goes to `late`. The result contract is untouched
|
|
431
|
+
* — this only decides when to stop waiting for one.
|
|
432
|
+
*/
|
|
433
|
+
function awaitExecutor(start, signal, options = {}) {
|
|
434
|
+
const contenders = [
|
|
435
|
+
Effect.suspend(() => {
|
|
436
|
+
let pending;
|
|
437
|
+
const settling = Effect.tryPromise({
|
|
438
|
+
try: () => (pending = Promise.resolve(start())),
|
|
439
|
+
catch: (err) => err,
|
|
440
|
+
});
|
|
441
|
+
const { late } = options;
|
|
442
|
+
return late
|
|
443
|
+
? settling.pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
444
|
+
pending?.then(late, () => { });
|
|
445
|
+
})))
|
|
446
|
+
: settling;
|
|
447
|
+
}),
|
|
448
|
+
// An executor that honors the signal rejects in the abort event itself,
|
|
449
|
+
// with the logs it captured attached, and that report beats ours by one
|
|
450
|
+
// timer turn. (A 1ms sleep, not 0: Effect.sleep(0) waits a microtask,
|
|
451
|
+
// which the executor's promise chain can still lose to.)
|
|
452
|
+
fromSignal(signal).pipe(Effect.catch(() => Effect.sleep(Duration.millis(1))), Effect.andThen(Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled.")))),
|
|
387
453
|
];
|
|
454
|
+
const { watchdog } = options;
|
|
455
|
+
if (watchdog) {
|
|
456
|
+
contenders.push(Effect.sleep(Duration.millis(watchdog.ms)).pipe(Effect.andThen(Effect.suspend(() => {
|
|
457
|
+
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", { watchdogMs: watchdog.ms });
|
|
458
|
+
return Effect.fail(new Error(`sandbox unresponsive: no outcome within the ${watchdog.ms}ms ` +
|
|
459
|
+
"execute.watchdogMs ceiling, so the run was abandoned"));
|
|
460
|
+
}))));
|
|
461
|
+
}
|
|
462
|
+
return Effect.raceAllFirst(contenders);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The run's own signal, aborted however the run ends. It follows the
|
|
466
|
+
* caller's, and aborting it on the way out is what releases outstanding host
|
|
467
|
+
* waits and tells cooperative connectors to stop.
|
|
468
|
+
*/
|
|
469
|
+
function runSignal(caller) {
|
|
470
|
+
return Effect.acquireRelease(Effect.sync(() => {
|
|
471
|
+
const controller = new AbortController();
|
|
472
|
+
const forward = () => controller.abort(caller?.reason);
|
|
473
|
+
if (caller?.aborted)
|
|
474
|
+
forward();
|
|
475
|
+
else
|
|
476
|
+
caller?.addEventListener("abort", forward, { once: true });
|
|
477
|
+
return { controller, forward };
|
|
478
|
+
}), ({ controller, forward }) => Effect.sync(() => {
|
|
479
|
+
controller.abort();
|
|
480
|
+
caller?.removeEventListener("abort", forward);
|
|
481
|
+
})).pipe(Effect.map(({ controller }) => controller.signal));
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* An admitting executor's lease, released when the run's scope closes.
|
|
485
|
+
*
|
|
486
|
+
* `acquire()` is the executor's Promise, awaited as one on purpose. A queued
|
|
487
|
+
* request is handed its lease from inside whichever request released one, so
|
|
488
|
+
* a fiber resumed straight from the executor's admission queue would build
|
|
489
|
+
* this request's providers and run its program in the releasing request —
|
|
490
|
+
* which workerd refuses for any I/O. Awaiting the promise resumes it here.
|
|
491
|
+
*/
|
|
492
|
+
function leased(executor, signal) {
|
|
493
|
+
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal }), signal, {
|
|
494
|
+
late: (lease) => lease.release(),
|
|
495
|
+
}), (lease) => Effect.sync(() => lease.release()));
|
|
388
496
|
}
|
|
389
497
|
/** The execute_code handler. Exported for direct testing. */
|
|
390
498
|
export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
let lease;
|
|
400
|
-
let outcome;
|
|
401
|
-
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
499
|
+
const watchdog = {
|
|
500
|
+
ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
|
|
501
|
+
logger,
|
|
502
|
+
};
|
|
503
|
+
return ({ code, diagnostics: diagnosticsRequested }, options = {}) => runEdge(Effect.suspend(() => {
|
|
504
|
+
const diagnostics = diagnosticsRequested
|
|
505
|
+
? new ExecuteDiagnostics()
|
|
506
|
+
: undefined;
|
|
402
507
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
403
508
|
const invocationFailures = [];
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
509
|
+
// The run's scope holds its signal and its lease. However the run
|
|
510
|
+
// ends — a result, a thrown executor, the watchdog, cancellation —
|
|
511
|
+
// closing it releases the lease and then aborts the signal, so
|
|
512
|
+
// nothing the run started outlives the request.
|
|
513
|
+
const run = Effect.gen(function* () {
|
|
514
|
+
const signal = yield* runSignal(options.signal);
|
|
515
|
+
// Admission comes before provider construction: queued calls retain
|
|
516
|
+
// no catalogs, request scopes, or provider closures.
|
|
517
|
+
let lease;
|
|
407
518
|
if (isAdmittingExecutor(executor)) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
finally {
|
|
413
|
-
if (diagnostics) {
|
|
414
|
-
diagnostics.admissionMs = Date.now() - admissionStarted;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
519
|
+
lease = yield* timed((elapsed) => {
|
|
520
|
+
if (diagnostics)
|
|
521
|
+
diagnostics.admissionMs = elapsed;
|
|
522
|
+
}, leased(executor, signal));
|
|
417
523
|
if ((lease.waitMs ?? 0) > 0) {
|
|
418
524
|
logger.debug("[connecta] execute_code admitted after queue wait", {
|
|
419
525
|
waitMs: lease.waitMs,
|
|
420
526
|
});
|
|
421
527
|
}
|
|
422
528
|
}
|
|
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 {
|
|
529
|
+
const provider = yield* timed((elapsed) => {
|
|
443
530
|
if (diagnostics)
|
|
444
|
-
diagnostics.setupMs =
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
531
|
+
diagnostics.setupMs = elapsed;
|
|
532
|
+
}, Effect.sync(() => sandboxProvider(registry, baseUrl, activity, {
|
|
533
|
+
signal,
|
|
534
|
+
onInvocationFailure: (failure) => {
|
|
535
|
+
invocationFailures.push(failure);
|
|
536
|
+
if (invocationFailures.length > 64)
|
|
537
|
+
invocationFailures.shift();
|
|
538
|
+
},
|
|
539
|
+
emitCollector: emitted,
|
|
540
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
541
|
+
discoveryConcurrency: config.discoveryConcurrency,
|
|
542
|
+
probeTimeoutMs: config.probeTimeoutMs,
|
|
543
|
+
maxHostCalls: config.maxHostCalls,
|
|
544
|
+
hostCallTimeoutMs: config.hostCallTimeoutMs,
|
|
545
|
+
defer: config.defer,
|
|
546
|
+
})));
|
|
547
|
+
if (signal.aborted) {
|
|
548
|
+
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
448
549
|
}
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
});
|
|
485
|
-
}
|
|
486
|
-
return failureResponse(`Executor failed: ${msg(err)}`, {
|
|
487
|
-
logs,
|
|
488
|
-
emitted,
|
|
489
|
-
diagnostics,
|
|
490
|
-
code: "executor_failed",
|
|
550
|
+
const admitted = lease;
|
|
551
|
+
// P1: a trailing `;` after the arrow expression breaks both
|
|
552
|
+
// executors' parenthesized evaluation. Strip it here, above them.
|
|
553
|
+
const program = withoutProgramTerminator(code);
|
|
554
|
+
return yield* timed((elapsed) => {
|
|
555
|
+
if (diagnostics)
|
|
556
|
+
diagnostics.executorWallMs = elapsed;
|
|
557
|
+
}, awaitExecutor(() => admitted
|
|
558
|
+
? admitted.execute(program, [provider])
|
|
559
|
+
: executor.execute(program, [provider]), signal, { watchdog }));
|
|
560
|
+
});
|
|
561
|
+
const reported = { emitted, diagnostics, invocationFailures };
|
|
562
|
+
return Effect.map(Effect.exit(Effect.scoped(run)), (exit) => Exit.isSuccess(exit)
|
|
563
|
+
? finishedRun(exit.value, reported)
|
|
564
|
+
: failedRun(Cause.squash(exit.cause), logger, reported));
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
567
|
+
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
|
568
|
+
function failedRun(err, logger, { emitted, diagnostics }) {
|
|
569
|
+
const logs = err !== null && typeof err === "object" && "logs" in err
|
|
570
|
+
? executeLogs(err.logs)
|
|
571
|
+
: undefined;
|
|
572
|
+
if (err instanceof ExecutorAdmissionError) {
|
|
573
|
+
if (err.code === "executor_overloaded") {
|
|
574
|
+
logger.warn("[connecta] execute_code admission rejected", {
|
|
575
|
+
code: err.code,
|
|
576
|
+
retryAfterMs: err.retryAfterMs,
|
|
491
577
|
});
|
|
492
578
|
}
|
|
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
|
-
|
|
579
|
+
return failureResponse(err.message, {
|
|
580
|
+
logs,
|
|
581
|
+
emitted: err instanceof ExecutorExecutionError ? emitted : undefined,
|
|
582
|
+
diagnostics,
|
|
583
|
+
code: {
|
|
584
|
+
code: err.code,
|
|
585
|
+
message: err.message,
|
|
586
|
+
retryable: err.retryable,
|
|
587
|
+
...(err.retryAfterMs !== undefined
|
|
588
|
+
? { retryAfterMs: err.retryAfterMs }
|
|
589
|
+
: {}),
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return failureResponse(`Executor failed: ${msg(err)}`, {
|
|
594
|
+
logs,
|
|
595
|
+
emitted,
|
|
596
|
+
diagnostics,
|
|
597
|
+
code: "executor_failed",
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
/** The response for an ExecuteResult: the program's error or its value. */
|
|
601
|
+
function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
|
|
602
|
+
const logs = executeLogs(outcome.logs);
|
|
603
|
+
if (outcome.error !== undefined) {
|
|
604
|
+
// Executor bridges necessarily reduce thrown host errors to strings.
|
|
605
|
+
// Match that terminal string back to the request-local typed failure so
|
|
606
|
+
// an unhandled tool failure keeps the same structured contract as
|
|
607
|
+
// call_tool. Failures caught by model code never reach
|
|
608
|
+
// outcome.error and therefore remain under that code's control.
|
|
609
|
+
//
|
|
610
|
+
// An error the program let through unchanged matches exactly, and an
|
|
611
|
+
// exact match always wins: a program that wrapped one failure's message
|
|
612
|
+
// around another's must not have the wrong type attached. Containment is
|
|
613
|
+
// the fallback, so a wrapped message still reports its underlying type
|
|
614
|
+
// rather than losing it to prose.
|
|
615
|
+
let invocationFailure;
|
|
616
|
+
for (const match of [
|
|
617
|
+
(candidate) => outcome.error !== "" &&
|
|
618
|
+
[candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
|
|
619
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) =>
|
|
620
|
+
// E6: empty or tiny prose cannot identify a wrapped failure.
|
|
621
|
+
message !== undefined &&
|
|
622
|
+
message.length >= 8 &&
|
|
623
|
+
outcome.error?.includes(message) === true),
|
|
624
|
+
]) {
|
|
625
|
+
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
626
|
+
const candidate = invocationFailures[i];
|
|
627
|
+
if (candidate && match(candidate)) {
|
|
628
|
+
invocationFailure = candidate;
|
|
531
629
|
break;
|
|
630
|
+
}
|
|
532
631
|
}
|
|
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);
|
|
632
|
+
if (invocationFailure)
|
|
633
|
+
break;
|
|
556
634
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return failureResponse(message, {
|
|
635
|
+
if (invocationFailure) {
|
|
636
|
+
// E1/X11: return the same bounded details the guest received.
|
|
637
|
+
return failureResponse(invocationFailure.details.message, {
|
|
560
638
|
logs,
|
|
561
639
|
emitted,
|
|
562
640
|
diagnostics,
|
|
563
|
-
code:
|
|
641
|
+
code: invocationFailure.details,
|
|
564
642
|
});
|
|
565
643
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
644
|
+
const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
|
|
645
|
+
return failureResponse(message, {
|
|
646
|
+
logs,
|
|
647
|
+
emitted,
|
|
648
|
+
diagnostics,
|
|
649
|
+
code: "executor_failed",
|
|
571
650
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
651
|
+
}
|
|
652
|
+
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
653
|
+
// value) makes JSON.stringify throw — keep that inside the structured
|
|
654
|
+
// error path so captured logs survive instead of a raw SDK 500.
|
|
655
|
+
let result;
|
|
656
|
+
try {
|
|
657
|
+
result = guardExecuteResultValue(outcome.result);
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
661
|
+
return failureResponse(message, {
|
|
662
|
+
logs,
|
|
663
|
+
emitted,
|
|
664
|
+
diagnostics,
|
|
665
|
+
code: "executor_failed",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const response = jsonResult({
|
|
669
|
+
result,
|
|
670
|
+
...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
|
|
671
|
+
...(logs ? { logs } : {}),
|
|
672
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
673
|
+
});
|
|
674
|
+
if (emitted.blocks.length > 0) {
|
|
675
|
+
// Emitted image/audio blocks are valid MCP content that ToolResult's
|
|
676
|
+
// text-only typing does not model — the same acknowledged gap
|
|
677
|
+
// guardContent lives with for downstream block passthrough.
|
|
678
|
+
response.content.push(...emitted.blocks);
|
|
679
|
+
}
|
|
680
|
+
return response;
|
|
580
681
|
}
|
|
581
682
|
function executeLogs(value) {
|
|
582
683
|
return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
|
|
@@ -662,6 +763,18 @@ Read relevant guides using top-level skills, not sandbox code. Write a plain-Jav
|
|
|
662
763
|
- console.log(...) is captured. Return data for the client to render.
|
|
663
764
|
|
|
664
765
|
No portable ambient capabilities. Return reduced JSON. If a provider result has an unfamiliar shape, return a small sample and continue in another call; never guess fields or use the whole text as an id. Top-level skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}; skills({ name: "investigate" }): task planning.`;
|
|
766
|
+
// Module scope, like the other six meta-tool inputs: its JSON Schema is
|
|
767
|
+
// derived once per process. Budgets and connectors vary by deployment and
|
|
768
|
+
// view, so they live in the description, never here.
|
|
769
|
+
const EXECUTE_INPUT = advertisedSchema(z.object({
|
|
770
|
+
code: z
|
|
771
|
+
.string()
|
|
772
|
+
.describe("One complete JavaScript async arrow function that discovers, calls, and returns the reduced answer."),
|
|
773
|
+
diagnostics: z
|
|
774
|
+
.boolean()
|
|
775
|
+
.optional()
|
|
776
|
+
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
777
|
+
}));
|
|
665
778
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
666
779
|
export function registerExecuteTool(server, registry, ctx) {
|
|
667
780
|
// Resolved once so the description and the collector cannot disagree about
|
|
@@ -684,19 +797,12 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
684
797
|
maxEmittedBlocks: emitBudgets.maxBlocks,
|
|
685
798
|
maxHostCalls: hostLimits.maxHostCalls,
|
|
686
799
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
800
|
+
watchdogMs: ctx.watchdogMs,
|
|
687
801
|
defer: ctx.defer,
|
|
688
802
|
});
|
|
689
803
|
server.registerTool("execute_code", {
|
|
690
804
|
description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors),
|
|
691
|
-
inputSchema:
|
|
692
|
-
code: z
|
|
693
|
-
.string()
|
|
694
|
-
.describe("One complete JavaScript async arrow function that discovers, calls, and returns the reduced answer."),
|
|
695
|
-
diagnostics: z
|
|
696
|
-
.boolean()
|
|
697
|
-
.optional()
|
|
698
|
-
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
699
|
-
}),
|
|
805
|
+
inputSchema: EXECUTE_INPUT,
|
|
700
806
|
// This hint describes connector calls, all explicitly read-only. The
|
|
701
807
|
// supported executor constructions deny outbound access, filesystem,
|
|
702
808
|
// and deployment config; X5 documents Dynamic runtime modules separately.
|