@zackbart/connecta 0.27.0 → 0.28.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 +6 -6
- package/CHANGELOG.md +56 -0
- package/README.md +15 -19
- package/bin/connecta.mjs +1 -5
- package/dist/activity.d.ts +5 -4
- package/dist/activity.js +1 -10
- package/dist/artifacts/json.js +10 -0
- package/dist/artifacts/operations.js +1 -1
- package/dist/artifacts/validate.js +1 -2
- package/dist/catalog-service.js +1 -1
- package/dist/execute.js +92 -368
- package/dist/executors/quickjs.js +1 -1
- package/dist/exempt-writes.js +162 -0
- package/dist/index.d.ts +12 -31
- package/dist/index.js +14 -47
- package/dist/invocation.js +6 -19
- package/dist/meta-tools.js +1 -2
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -3
- package/dist/operator-ui/view.js +6 -4
- package/dist/routes/mcp.js +5 -21
- package/dist/routes/shared.d.ts +1 -6
- package/dist/server.js +0 -4
- package/dist/skills.js +8 -23
- package/dist/tool-safety.d.ts +1 -1
- package/dist/tool-safety.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +10 -14
- package/documentation/code-mode.md +91 -194
- package/documentation/meta-tools.md +38 -41
- package/ethos.md +6 -7
- package/examples/worker/README.md +10 -14
- package/examples/worker/src/d1-activity-row.ts +4 -1
- package/examples/worker/src/index.ts +1 -9
- package/package.json +2 -2
- package/templates/node/AGENTS.md +4 -3
- package/templates/node/README.md +3 -4
- package/templates/node/package.json +1 -1
- package/dist/resumable.js +0 -1323
- package/dist/run-journal.js +0 -347
package/dist/execute.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause,
|
|
1
|
+
import { Cause, Duration, Effect, Exit } from "effect";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
4
4
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
@@ -6,10 +6,9 @@ import { errorResult, jsonResult } from "./meta-tools.js";
|
|
|
6
6
|
import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
|
|
7
7
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
|
|
8
8
|
import { boundedEchoText, msg } from "./errors.js";
|
|
9
|
+
import { DEFAULT_MAX_WRITES, ExemptWrites, writeStateOf, } from "./exempt-writes.js";
|
|
9
10
|
import { InvocationFailure, InvocationService, timed, } from "./invocation.js";
|
|
10
11
|
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
12
|
import { underAnySignal } from "./timeout.js";
|
|
14
13
|
import { isApprovalExempt, NO_EXEMPTIONS, } from "./tool-safety.js";
|
|
15
14
|
import { fromSignal, runEdge } from "./runtime/run.js";
|
|
@@ -278,129 +277,6 @@ function guestErrorPrelude(failureSecret) {
|
|
|
278
277
|
});
|
|
279
278
|
})(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
|
|
280
279
|
}
|
|
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
|
-
}
|
|
404
280
|
/**
|
|
405
281
|
* Expose one host provider and typed guest errors. Catalogs load only when
|
|
406
282
|
* the program calls a tool or asks search/describe.
|
|
@@ -415,7 +291,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
415
291
|
* edge the executor awaits, and behind it one host call is one fiber: spend
|
|
416
292
|
* the budget, do the operation, and turn a typed failure into the frame the
|
|
417
293
|
* prelude rebuilds inside the guest. Nothing is shared between those fibers
|
|
418
|
-
* but the budget
|
|
294
|
+
* but the budget counters and this request's catalog, so a call the program
|
|
419
295
|
* never awaits cannot disturb the others.
|
|
420
296
|
*/
|
|
421
297
|
function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
@@ -435,26 +311,28 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
435
311
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
436
312
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
437
313
|
const failureSecret = guestFailureSecret();
|
|
438
|
-
const { signal, diagnostics } = limits;
|
|
314
|
+
const { signal, diagnostics, exemptWrites } = limits;
|
|
439
315
|
let hostCalls = 0;
|
|
440
316
|
// L4/M7: discovery and invocation spend the same budget, on entry; emit
|
|
441
317
|
// does not.
|
|
442
318
|
const spendHostCall = Effect.suspend(() => ++hostCalls > maxHostCalls
|
|
443
319
|
? Effect.fail(guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`))
|
|
444
320
|
: Effect.void);
|
|
445
|
-
const { runState, exemptWrites } = limits;
|
|
446
321
|
const approval = limits.approval ?? NO_EXEMPTIONS;
|
|
447
322
|
const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
|
|
448
|
-
let
|
|
323
|
+
let writes = 0;
|
|
449
324
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
* it rather than abort it.
|
|
325
|
+
* The write gate covers exactly the config-exempt calls (#566): each spends
|
|
326
|
+
* the write budget and is tracked until it settles (`ExemptWrites`), so the
|
|
327
|
+
* play can wait for it rather than abort it. Every other write keeps E4's
|
|
328
|
+
* refusal, ahead of validation, so the host's prompt on
|
|
329
|
+
* call_destructive_tool stays the only approval there is.
|
|
456
330
|
*/
|
|
457
|
-
const
|
|
331
|
+
const invocationContext = (sending) => ({
|
|
332
|
+
source: "execute_code",
|
|
333
|
+
timeoutMs: hostCallTimeoutMs,
|
|
334
|
+
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
335
|
+
unwrapResult: true,
|
|
458
336
|
gates: (target) => isApprovalExempt(approval, target.connector, target.toolName, target.definition),
|
|
459
337
|
writeGate: (target) => Effect.sync(() => {
|
|
460
338
|
if (exemptWrites?.isClosed) {
|
|
@@ -465,10 +343,10 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
465
343
|
message: "The program had already returned, so this write was not sent.",
|
|
466
344
|
retryable: false,
|
|
467
345
|
},
|
|
468
|
-
|
|
346
|
+
unrecorded: true,
|
|
469
347
|
};
|
|
470
348
|
}
|
|
471
|
-
if (
|
|
349
|
+
if (writes >= maxWrites) {
|
|
472
350
|
return {
|
|
473
351
|
kind: "refuse",
|
|
474
352
|
error: {
|
|
@@ -478,89 +356,22 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
478
356
|
},
|
|
479
357
|
};
|
|
480
358
|
}
|
|
481
|
-
|
|
359
|
+
writes++;
|
|
482
360
|
if (exemptWrites)
|
|
483
361
|
sending.settle = exemptWrites.begin();
|
|
484
362
|
return { kind: "dispatch" };
|
|
485
363
|
}),
|
|
486
364
|
});
|
|
487
|
-
const invocationContext = (seq, key, sending) => ({
|
|
488
|
-
source: runState?.source ?? "execute_code",
|
|
489
|
-
timeoutMs: hostCallTimeoutMs,
|
|
490
|
-
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
491
|
-
unwrapResult: true,
|
|
492
|
-
// With resumable writes, a consequential call reaches the run's gate
|
|
493
|
-
// instead of the flat E4 refusal, and a read's decision is marked the
|
|
494
|
-
// moment it passes the same point.
|
|
495
|
-
...(runState && seq !== undefined && key !== undefined
|
|
496
|
-
? {
|
|
497
|
-
beforeDispatch: () => runState.decide(seq),
|
|
498
|
-
writeGate: (target, args) => runState.gate(seq, key, target, args),
|
|
499
|
-
}
|
|
500
|
-
: runState
|
|
501
|
-
? {}
|
|
502
|
-
: exemptOnly(sending)),
|
|
503
|
-
});
|
|
504
|
-
/**
|
|
505
|
-
* The front of every numbered host call once resumable writes are on: a
|
|
506
|
-
* stopped run makes no call at all (not journaled, no activity), a
|
|
507
|
-
* recorded one is answered from the journal before the budget-free part
|
|
508
|
-
* of the path, and one the journal cannot account for is divergence.
|
|
509
|
-
* Replayed calls spend the host-call budget again, so a play's total is
|
|
510
|
-
* the run's total and nothing is counted twice.
|
|
511
|
-
*/
|
|
512
|
-
const fromJournal = (seq, op, address, args) => Effect.gen(function* () {
|
|
513
|
-
const halted = runState?.halted();
|
|
514
|
-
if (halted)
|
|
515
|
-
return yield* Effect.fail(halted);
|
|
516
|
-
yield* spendHostCall;
|
|
517
|
-
if (!runState || seq === undefined)
|
|
518
|
-
return { kind: "live", key: undefined };
|
|
519
|
-
const key = journalKey(op, address, args);
|
|
520
|
-
const found = runState.lookup(key);
|
|
521
|
-
if (found.kind === "diverged") {
|
|
522
|
-
return yield* Effect.fail(runState.diverge(found.reason));
|
|
523
|
-
}
|
|
524
|
-
if (found.kind === "live")
|
|
525
|
-
return { kind: "live", key };
|
|
526
|
-
runState.decide(seq);
|
|
527
|
-
const { outcome } = found.entry;
|
|
528
|
-
return outcome.ok
|
|
529
|
-
? { kind: "replayed", value: outcome.value }
|
|
530
|
-
: yield* Effect.fail(new InvocationFailure(outcome.error));
|
|
531
|
-
});
|
|
532
365
|
// A call brings its own cancellation: the invocation pipeline reads the
|
|
533
366
|
// run's signal, refuses a call that starts after it, and records the
|
|
534
367
|
// cancelled attempt in activity like any other outcome.
|
|
535
|
-
const call = (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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.
|
|
368
|
+
const call = (address, args) => Effect.gen(function* () {
|
|
369
|
+
yield* spendHostCall;
|
|
370
|
+
// An exempt write settles here — unknown if the call never returned an
|
|
371
|
+
// outcome.
|
|
543
372
|
const sending = {};
|
|
544
|
-
const outcome = yield* invocation.pipeline(
|
|
373
|
+
const outcome = yield* invocation.pipeline(String(address), args ?? {}, invocationContext(sending)).pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
|
|
545
374
|
diagnostics?.recordCall(outcome);
|
|
546
|
-
if (runState && seq !== undefined && front.key !== undefined) {
|
|
547
|
-
if (runState.isLiveWrite(seq)) {
|
|
548
|
-
const resolved = outcome.resolved;
|
|
549
|
-
const replaced = yield* runState.settleWrite(seq, front.key, {
|
|
550
|
-
address: resolved
|
|
551
|
-
? `${resolved.connector.id}.${resolved.toolName}`
|
|
552
|
-
: addressText,
|
|
553
|
-
args: callArgs,
|
|
554
|
-
}, outcome);
|
|
555
|
-
if (replaced)
|
|
556
|
-
return yield* Effect.fail(replaced);
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
runState.record(seq, "call", front.key, outcome.ok
|
|
560
|
-
? { ok: true, value: outcome.value }
|
|
561
|
-
: { ok: false, error: outcome.error });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
375
|
return outcome.ok
|
|
565
376
|
? outcome.value
|
|
566
377
|
: yield* Effect.fail(new InvocationFailure(outcome.error));
|
|
@@ -570,7 +381,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
570
381
|
// `cancelled` instead of holding the program until its probe deadline.
|
|
571
382
|
// Policy failures use the same thrown vocabulary as calls and utilities;
|
|
572
383
|
// the transport below reconstructs their code inside the guest.
|
|
573
|
-
const discovery = (operation,
|
|
384
|
+
const discovery = (operation, read) => Effect.suspend(() => {
|
|
574
385
|
const started = Date.now();
|
|
575
386
|
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
576
387
|
const reading = Effect.tryPromise({
|
|
@@ -579,49 +390,22 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
579
390
|
? guestFailure(err.code, err.message)
|
|
580
391
|
: err,
|
|
581
392
|
});
|
|
582
|
-
return
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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))));
|
|
393
|
+
return spendHostCall.pipe(Effect.andThen(!signal
|
|
394
|
+
? reading
|
|
395
|
+
: signal.aborted
|
|
396
|
+
? Effect.fail(cancelled())
|
|
397
|
+
: Effect.raceAllFirst([
|
|
398
|
+
reading,
|
|
399
|
+
fromSignal(signal).pipe(Effect.mapError(cancelled)),
|
|
400
|
+
])), Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
616
401
|
});
|
|
617
402
|
const operations = {
|
|
618
403
|
call,
|
|
619
404
|
// Emission is a provider function, never an ExecuteResult field —
|
|
620
405
|
// that is what keeps the Executor contract untouched and parity
|
|
621
406
|
// structural (M8). It spends no host-call budget (M7); its own
|
|
622
|
-
// budgets live in the collector.
|
|
623
|
-
|
|
624
|
-
emit: (_seq, block) => Effect.try({
|
|
407
|
+
// budgets live in the collector.
|
|
408
|
+
emit: (block) => Effect.try({
|
|
625
409
|
try: () => {
|
|
626
410
|
if (!limits.emitCollector) {
|
|
627
411
|
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
@@ -630,7 +414,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
630
414
|
},
|
|
631
415
|
catch: (err) => err,
|
|
632
416
|
}),
|
|
633
|
-
search: (
|
|
417
|
+
search: (raw) => discovery("search", async () => {
|
|
634
418
|
const args = (raw ?? {});
|
|
635
419
|
const result = flatSearchResult(await catalog.search({
|
|
636
420
|
...args,
|
|
@@ -639,7 +423,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
639
423
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
640
424
|
return result;
|
|
641
425
|
}),
|
|
642
|
-
describe: (
|
|
426
|
+
describe: (raw) => discovery("describe", async () => {
|
|
643
427
|
const args = (raw ?? {});
|
|
644
428
|
const result = { tools: await catalog.describe(args) };
|
|
645
429
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
@@ -659,22 +443,12 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
659
443
|
});
|
|
660
444
|
return {
|
|
661
445
|
name: "connecta",
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
prelude: `${guestErrorPrelude(failureSecret)}\n${pinnedEnvironmentPrelude(runState?.environment ?? limits.environment ?? freshEnvironment())}`,
|
|
446
|
+
// Trusted host code the program runs after and cannot undo.
|
|
447
|
+
prelude: guestErrorPrelude(failureSecret),
|
|
665
448
|
fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
|
|
666
449
|
name,
|
|
667
450
|
(...args) => {
|
|
668
|
-
|
|
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);
|
|
451
|
+
const settled = runEdge(Effect.suspend(() => operation(...args)).pipe(Effect.catch(framed)));
|
|
678
452
|
// The executor owns this promise, and a program may abandon a call
|
|
679
453
|
// that the run's end then cancels before anyone has awaited it.
|
|
680
454
|
// The rejection is still there for whoever does; it is just not an
|
|
@@ -770,34 +544,32 @@ function leased(executor, signal) {
|
|
|
770
544
|
late: (lease) => lease.release(),
|
|
771
545
|
}), (lease) => Effect.sync(() => lease.release()));
|
|
772
546
|
}
|
|
547
|
+
/** Room for the executor queue a run waits in before its watchdog starts. */
|
|
548
|
+
const RUN_CLAIM_SLACK_MS = 10_000;
|
|
773
549
|
/**
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* still keeps any write from being sent twice.
|
|
550
|
+
* How long one run may take before whoever claimed it may give up on it: the
|
|
551
|
+
* watchdog, one more host-call deadline for an exempt write the play drains,
|
|
552
|
+
* and queue slack. Artifact refresh leases its claim for this long.
|
|
778
553
|
*/
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
554
|
+
export function runClaimMs(config) {
|
|
555
|
+
return resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS) +
|
|
556
|
+
resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS) +
|
|
557
|
+
RUN_CLAIM_SLACK_MS;
|
|
558
|
+
}
|
|
559
|
+
/** The execute_code handler. Exported for direct testing and artifact refresh. */
|
|
560
|
+
export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
|
|
782
561
|
const watchdog = {
|
|
783
562
|
ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
|
|
784
563
|
logger,
|
|
785
564
|
};
|
|
786
565
|
const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
|
|
787
|
-
const
|
|
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(() => {
|
|
566
|
+
const play = (program, diagnosticsRequested, callerSignal) => Effect.suspend(() => {
|
|
795
567
|
const diagnostics = diagnosticsRequested
|
|
796
568
|
? new ExecuteDiagnostics()
|
|
797
569
|
: undefined;
|
|
798
570
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
799
571
|
const invocationFailures = [];
|
|
800
|
-
const exemptWrites =
|
|
572
|
+
const exemptWrites = new ExemptWrites();
|
|
801
573
|
// The run's scope holds its signal and its lease. However the run
|
|
802
574
|
// ends — a result, a thrown executor, the watchdog, cancellation —
|
|
803
575
|
// closing it releases the lease and then aborts the signal, so
|
|
@@ -835,8 +607,6 @@ export function createProgramRunner(registry, baseUrl, executor, logger, activit
|
|
|
835
607
|
maxHostCalls: config.maxHostCalls,
|
|
836
608
|
hostCallTimeoutMs,
|
|
837
609
|
defer: config.defer,
|
|
838
|
-
environment: config.environment,
|
|
839
|
-
runState,
|
|
840
610
|
approval: config.approval,
|
|
841
611
|
maxWrites: config.maxWrites,
|
|
842
612
|
exemptWrites,
|
|
@@ -845,92 +615,48 @@ export function createProgramRunner(registry, baseUrl, executor, logger, activit
|
|
|
845
615
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
846
616
|
}
|
|
847
617
|
const admitted = lease;
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
// then let it finish before the scope aborts it.
|
|
854
|
-
return yield* timed((elapsed) => {
|
|
855
|
-
if (diagnostics)
|
|
856
|
-
diagnostics.executorWallMs = elapsed;
|
|
857
|
-
}, executing.pipe(Effect.ensuring(Effect.suspend(() => {
|
|
858
|
-
exemptWrites?.close();
|
|
859
|
-
return exemptWrites?.drain() ?? Effect.void;
|
|
860
|
-
}))));
|
|
861
|
-
}
|
|
862
|
-
// The run stopping wins the race as soon as its gate decides, before
|
|
863
|
-
// the program even sees the rejection. Either way the play closes —
|
|
864
|
-
// a write gated after this is not sent — and what is still on the
|
|
865
|
-
// wire finishes before the scope aborts it: every call after a stop,
|
|
866
|
-
// since each is journaled, and every write otherwise, since an
|
|
867
|
-
// abandoned write would be an unknown one.
|
|
868
|
-
const ended = yield* timed((elapsed) => {
|
|
618
|
+
// An exempt write may still be on the wire when the program settles:
|
|
619
|
+
// close, so a write gated after this is not sent, then let the ones
|
|
620
|
+
// already dispatched finish before the scope aborts them — an
|
|
621
|
+
// abandoned write would be one whose outcome nobody knows.
|
|
622
|
+
return yield* timed((elapsed) => {
|
|
869
623
|
if (diagnostics)
|
|
870
624
|
diagnostics.executorWallMs = elapsed;
|
|
871
|
-
},
|
|
872
|
-
|
|
873
|
-
|
|
625
|
+
}, awaitExecutor(() => admitted
|
|
626
|
+
? admitted.execute(program, [provider])
|
|
627
|
+
: executor.execute(program, [provider]), signal, { watchdog }).pipe(Effect.ensuring(Effect.suspend(() => {
|
|
628
|
+
exemptWrites.close();
|
|
629
|
+
return exemptWrites.drain();
|
|
874
630
|
}))));
|
|
875
|
-
if ("stop" in ended)
|
|
876
|
-
yield* runState.drain("all");
|
|
877
|
-
return ended;
|
|
878
631
|
});
|
|
879
632
|
const reported = { emitted, diagnostics, invocationFailures };
|
|
880
|
-
return Effect.
|
|
633
|
+
return Effect.map(Effect.exit(Effect.scoped(run)), (exit) => {
|
|
881
634
|
if (Exit.isFailure(exit)) {
|
|
882
|
-
|
|
883
|
-
return runState
|
|
884
|
-
? runState.finishExecutorFailure(failed)
|
|
885
|
-
: Effect.succeed(exemptWrites?.finish(failed) ?? failed);
|
|
635
|
+
return exemptWrites.finish(failedRun(Cause.squash(exit.cause), logger, reported));
|
|
886
636
|
}
|
|
887
|
-
const
|
|
888
|
-
if ("stop" in ended) {
|
|
889
|
-
return runState
|
|
890
|
-
? runState.finishStopped()
|
|
891
|
-
: Effect.die(new Error("a run without state cannot stop"));
|
|
892
|
-
}
|
|
893
|
-
const finished = finishedRun(ended.settled, reported);
|
|
637
|
+
const finished = finishedRun(exit.value, reported);
|
|
894
638
|
if (config.failOnInvocationFailure && invocationFailures.length > 0) {
|
|
895
639
|
const refusal = invocationFailures[0];
|
|
896
|
-
return
|
|
640
|
+
return failureResponse(refusal.details.message, {
|
|
897
641
|
code: refusal.details,
|
|
898
|
-
})
|
|
642
|
+
});
|
|
899
643
|
}
|
|
900
|
-
return
|
|
901
|
-
? runState.finishSettled(finished)
|
|
902
|
-
: Effect.succeed(exemptWrites?.finish(finished) ?? finished);
|
|
644
|
+
return exemptWrites.finish(finished);
|
|
903
645
|
});
|
|
904
646
|
});
|
|
905
|
-
return {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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);
|
|
647
|
+
return ({ code, diagnostics }, options = {}) => {
|
|
648
|
+
// A code-unit count above the cap is already too large in UTF-8. Check
|
|
649
|
+
// that first so a huge direct-call string is never encoded in full.
|
|
650
|
+
if (code.length > EXECUTE_MAX_CODE_BYTES ||
|
|
651
|
+
new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
|
|
652
|
+
const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
|
|
653
|
+
return Promise.resolve(failureResponse(message, {
|
|
654
|
+
code: { code: "invalid_args", message, retryable: false },
|
|
925
655
|
}));
|
|
926
|
-
}
|
|
927
|
-
|
|
656
|
+
}
|
|
657
|
+
return runEdge(Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal)));
|
|
928
658
|
};
|
|
929
659
|
}
|
|
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
660
|
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
|
935
661
|
function failedRun(err, logger, { emitted, diagnostics }) {
|
|
936
662
|
const logs = err !== null && typeof err === "object" && "logs" in err
|
|
@@ -1132,14 +858,12 @@ function connectorInventory(connectors) {
|
|
|
1132
858
|
return `${prefix}${shown.join(", ")}.`;
|
|
1133
859
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
1134
860
|
}
|
|
1135
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors,
|
|
1136
|
-
? `Writes pause for resume_execution. Keep an authorized batch in one program; approval "tool" covers later same-address calls, other approval-required writes pause separately. Limits: ${hostLimits.maxHostCalls} host calls, ${resumable.maxWrites} writes`
|
|
1137
|
-
: `Only readOnlyHint: true tools are available. Limits: ${hostLimits.maxHostCalls} host calls`}, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
861
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites) => `Use the configured services below to answer the task. A known address uses call_tool. Unknown-address and wider read-only work uses one execute_code program for discovery, calls, and reduction. Do not return catalog matches alone. Only readOnlyHint: true tools and config-exempt writes are available; any other write goes through call_destructive_tool. Limits: ${hostLimits.maxHostCalls} host calls, ${maxWrites} exempt writes, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
1138
862
|
|
|
1139
863
|
${connectorInventory(connectors)}
|
|
1140
864
|
|
|
1141
865
|
Read guides with top-level skills. Write async () => { ... } using the global connecta:
|
|
1142
|
-
- connecta.search({ connector, query,
|
|
866
|
+
- connecta.search({ connector, query, safety: "readOnly", includeSchemas: "json" }) returns { tools }. Search operations separately; choose by connectorTitle and schema.required/.properties, never guessed fields. Compact schemas are text.
|
|
1143
867
|
- connecta.describe({ address }) clarifies schemas.
|
|
1144
868
|
- connecta.call(address, args) returns the provider value directly.
|
|
1145
869
|
- Use Promise.all for independent calls, or Promise.allSettled to retain failures. Check status; missing values are unknown, never false or zero.
|
|
@@ -1173,7 +897,8 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1173
897
|
hostCallTimeoutMs: resolveBudget(ctx.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS),
|
|
1174
898
|
};
|
|
1175
899
|
const connectors = registry.listConnectors();
|
|
1176
|
-
const
|
|
900
|
+
const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
|
|
901
|
+
const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
|
|
1177
902
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
1178
903
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1179
904
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
@@ -1182,26 +907,25 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1182
907
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
1183
908
|
watchdogMs: ctx.watchdogMs,
|
|
1184
909
|
defer: ctx.defer,
|
|
1185
|
-
resumable: ctx.resumable,
|
|
1186
910
|
approval: ctx.approval,
|
|
1187
|
-
maxWrites
|
|
911
|
+
maxWrites,
|
|
1188
912
|
});
|
|
1189
913
|
server.registerTool("execute_code", {
|
|
1190
|
-
description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors,
|
|
914
|
+
description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors, maxWrites),
|
|
1191
915
|
inputSchema: EXECUTE_INPUT,
|
|
1192
916
|
// This hint describes connector calls: only explicitly read-only ones
|
|
1193
|
-
// run here,
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1197
|
-
//
|
|
917
|
+
// run here, plus the writes config exempts from asking (W12) — the
|
|
918
|
+
// deployment's decision, which no annotation can make for it. Any
|
|
919
|
+
// other write is refused (E4) and crosses call_destructive_tool, which
|
|
920
|
+
// is annotated destructive. The supported executor constructions deny
|
|
921
|
+
// outbound access, filesystem, and deployment config; X5 documents
|
|
922
|
+
// Dynamic runtime modules separately.
|
|
1198
923
|
annotations: {
|
|
1199
924
|
readOnlyHint: true,
|
|
1200
925
|
destructiveHint: false,
|
|
1201
926
|
openWorldHint: true,
|
|
1202
927
|
},
|
|
1203
|
-
}, (args, extra) => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) =>
|
|
928
|
+
}, (args, extra) => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
|
|
1204
929
|
signal,
|
|
1205
930
|
})));
|
|
1206
|
-
return runner;
|
|
1207
931
|
}
|