@zackbart/connecta 0.41.0 → 0.42.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/CHANGELOG.md +61 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -0
- package/dist/invocation.js +98 -34
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +1 -1
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/execute.js
CHANGED
|
@@ -2,6 +2,7 @@ import { operationActivity } from "./operation-activity.js";
|
|
|
2
2
|
import { SkillLookupError } from "./skills.js";
|
|
3
3
|
import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
|
|
4
4
|
import { META_ROUTES, routeNames } from "./agent-routes.js";
|
|
5
|
+
import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
|
|
5
6
|
import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
|
|
6
7
|
import { nativeRecovery } from "./native-recovery.js";
|
|
7
8
|
import { GUEST_API_DECLARATION, GUEST_FUNCTIONS, UNCOUNTED_GUEST_FUNCTIONS, } from "./guest-functions.js";
|
|
@@ -11,29 +12,38 @@ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
|
|
|
11
12
|
import * as z from "zod";
|
|
12
13
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
13
14
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
14
|
-
import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
|
|
15
|
-
import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
|
|
15
|
+
import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, releaseStashedResult, } from "./meta-tools.js";
|
|
16
|
+
import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_READ_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
|
|
16
17
|
import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
|
|
17
18
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
|
|
18
|
-
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
19
|
+
import { boundedEchoText, clipSerialized, ConnectorCallError, msg } from "./errors.js";
|
|
19
20
|
import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
|
|
20
21
|
import { InvocationFailure, InvocationService, timed } from "./invocation.js";
|
|
21
22
|
import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
|
|
22
23
|
import { EXECUTE_OUTPUT } from "./meta-output.js";
|
|
23
24
|
import { normalizeProgramSource } from "./program-source.js";
|
|
24
25
|
import { readResource } from "./resource-read.js";
|
|
26
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
27
|
+
import { HeldResults, sealHeldResults } from "./result-seal.js";
|
|
25
28
|
import { redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
|
|
26
29
|
import { normalizeTimeoutMs } from "./timeout.js";
|
|
27
30
|
import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
28
31
|
import { closeScope } from "./runtime/connector-scope.js";
|
|
29
32
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
30
33
|
import { executorProgram } from "./runtime/executor.js";
|
|
31
|
-
import { connectorGuide, connectorGuideRequired,
|
|
34
|
+
import { connectorGuide, connectorGuideRequired, addsInformation, connectorInventoryTitle, inventoryNote, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
|
|
32
35
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
33
36
|
const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
|
|
34
37
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
35
38
|
/** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
|
|
36
39
|
const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
40
|
+
/** All of one run's stash reclaim, in milliseconds of waiting on storage. */
|
|
41
|
+
const RECLAIM_BUDGET_MS = 1_000;
|
|
42
|
+
/**
|
|
43
|
+
* Reclaims per run. Each freed slot lets another read stash and page again, so unbounded
|
|
44
|
+
* reclaim keeps a program spending storage queries that a full stash would have ended.
|
|
45
|
+
*/
|
|
46
|
+
const RECLAIM_MAX_PER_RUN = 3;
|
|
37
47
|
/**
|
|
38
48
|
* The outer ceiling on one execution. It sits above both executors' own
|
|
39
49
|
* deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
|
|
@@ -41,6 +51,41 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
|
41
51
|
* never settles at all.
|
|
42
52
|
*/
|
|
43
53
|
const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
|
|
54
|
+
/**
|
|
55
|
+
* How long before a deadline a program's reads stop: the time the guest gets
|
|
56
|
+
* to receive their `timeout` and return what it has.
|
|
57
|
+
*/
|
|
58
|
+
const READ_DEADLINE_MARGIN_MS = 1_000;
|
|
59
|
+
const readMarginMs = (deadlineMs) => Math.min(READ_DEADLINE_MARGIN_MS, Math.floor(deadlineMs / 10));
|
|
60
|
+
/**
|
|
61
|
+
* When a run's reads stop: before the executor's deadline, measured from now,
|
|
62
|
+
* and before the response ceiling, which queueing may already have spent.
|
|
63
|
+
*/
|
|
64
|
+
function readDeadlineAt(executor, ceiling) {
|
|
65
|
+
const beforeCeiling = ceiling.at - readMarginMs(ceiling.ms);
|
|
66
|
+
const { deadlineMs } = executor;
|
|
67
|
+
if (deadlineMs === undefined)
|
|
68
|
+
return beforeCeiling;
|
|
69
|
+
return Math.min(beforeCeiling, Date.now() + deadlineMs - readMarginMs(deadlineMs));
|
|
70
|
+
}
|
|
71
|
+
/** Past the ceiling, room to store what a run leaves behind. Default 57 s against a 60 s client. */
|
|
72
|
+
const RESPONSE_STORAGE_MARGIN_MS = 2_000;
|
|
73
|
+
function responseCeiling(startedAt, ms) {
|
|
74
|
+
const at = startedAt + ms;
|
|
75
|
+
return { ms, at, startedAt, answerBy: at + Math.min(RESPONSE_STORAGE_MARGIN_MS, Math.floor(ms / 10)) };
|
|
76
|
+
}
|
|
77
|
+
/** Refuse a run with no time left to start. Nothing has run, so running it again is safe. */
|
|
78
|
+
const startable = (ceiling) => Effect.suspend(() => {
|
|
79
|
+
const elapsedMs = Date.now() - ceiling.startedAt;
|
|
80
|
+
return elapsedMs < ceiling.ms
|
|
81
|
+
? Effect.void
|
|
82
|
+
: Effect.fail(new InvocationFailure({
|
|
83
|
+
code: "timeout",
|
|
84
|
+
message: `Operation "execute_code" used its ${ceiling.ms}ms response ceiling before it started, so it did not run and sent nothing. Run it again.`,
|
|
85
|
+
retryable: true,
|
|
86
|
+
details: { operation: "execute_code", stage: "response ceiling", elapsedMs, deadlineMs: ceiling.ms },
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
44
89
|
/**
|
|
45
90
|
* Room to name every connector of a typical deployment. Past it, complete
|
|
46
91
|
* entries plus an exact omission count stay inside this byte cap.
|
|
@@ -351,31 +396,16 @@ function guestFailure(code, message, retryable = false) {
|
|
|
351
396
|
}
|
|
352
397
|
/**
|
|
353
398
|
* E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
|
|
399
|
+
* The guest keeps this text, and may return it after a later call registers a credential it
|
|
400
|
+
* holds, so the message is cut between tokens.
|
|
354
401
|
* Keep optional recovery whole: clipping an address or echoed args could turn
|
|
355
402
|
* recovery into a different call. Oversized metadata is omitted instead.
|
|
356
403
|
*/
|
|
357
404
|
function boundedGuestFailure(failure) {
|
|
358
|
-
const boundText = (text, maxChars) => {
|
|
359
|
-
if (JSON.stringify(text).length <= maxChars)
|
|
360
|
-
return text;
|
|
361
|
-
let low = 0;
|
|
362
|
-
let high = Math.min(text.length, maxChars);
|
|
363
|
-
while (low < high) {
|
|
364
|
-
const mid = Math.ceil((low + high) / 2);
|
|
365
|
-
if (JSON.stringify(`${text.slice(0, mid)}…`).length <= maxChars)
|
|
366
|
-
low = mid;
|
|
367
|
-
else
|
|
368
|
-
high = mid - 1;
|
|
369
|
-
}
|
|
370
|
-
// Do not manufacture a lone surrogate when clipping a Unicode message.
|
|
371
|
-
if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1]))
|
|
372
|
-
low--;
|
|
373
|
-
return `${text.slice(0, low)}…`;
|
|
374
|
-
};
|
|
375
405
|
let details = {
|
|
376
406
|
...failure.details,
|
|
377
|
-
code:
|
|
378
|
-
message:
|
|
407
|
+
code: clipSerialized(failure.details.code, 128),
|
|
408
|
+
message: clipSerialized(failure.details.message, 2_000),
|
|
379
409
|
};
|
|
380
410
|
// Keep typed metadata below the executor bridge bound.
|
|
381
411
|
if (JSON.stringify(details).length > 3_700) {
|
|
@@ -401,7 +431,7 @@ class HostCallBudgetExceeded extends InvocationFailure {
|
|
|
401
431
|
constructor(hostCalls, maxHostCalls) {
|
|
402
432
|
super({
|
|
403
433
|
code: "budget_exceeded",
|
|
404
|
-
message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)
|
|
434
|
+
message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum); the run returned nothing. Split the rest across programs of at most ${maxHostCalls} calls each; do not resend any write it sent.`,
|
|
405
435
|
retryable: false,
|
|
406
436
|
});
|
|
407
437
|
this.hostCalls = hostCalls;
|
|
@@ -431,6 +461,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
431
461
|
readOnly: limits.readOnly,
|
|
432
462
|
// A program that just missed an address cannot call search_tools.
|
|
433
463
|
searchRoute: "connecta.search",
|
|
464
|
+
markUncallableWrites: limits.readProgram,
|
|
434
465
|
concurrency: limits.discoveryConcurrency,
|
|
435
466
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
436
467
|
defer: limits.defer,
|
|
@@ -440,6 +471,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
440
471
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
441
472
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
442
473
|
const { signal, diagnostics, programWrites } = limits;
|
|
474
|
+
/** The part of `requestedMs` left before reads stop; never below one tick. */
|
|
475
|
+
const readBudget = (requestedMs) => Math.min(requestedMs, Math.max(1, (limits.readDeadlineAt ?? Infinity) - Date.now()));
|
|
443
476
|
const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
444
477
|
let budgetFailure;
|
|
445
478
|
let emitAttempts = 0;
|
|
@@ -460,33 +493,70 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
460
493
|
const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
|
|
461
494
|
let writes = 0;
|
|
462
495
|
// These are transfer cursors only. All result bytes live in the existing stash.
|
|
496
|
+
// `reclaim` marks a read's stash only this transfer can name, because the guest never saw
|
|
497
|
+
// its handle. A guest-supplied id stays readable until it expires; a write's stash expires too.
|
|
498
|
+
const readBytes = limits.readBytes ?? PROGRAM_RESULT_READ_BYTES;
|
|
463
499
|
const transfers = new Map();
|
|
464
500
|
const nativeResults = new Map();
|
|
465
|
-
const transportResult = (value) => {
|
|
501
|
+
const transportResult = (value, write) => {
|
|
466
502
|
if (value.format === "paged") {
|
|
467
503
|
const handle = value;
|
|
468
504
|
if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
|
|
469
505
|
const transfer = `transfer:${crypto.randomUUID()}`;
|
|
470
|
-
transfers.set(transfer, {
|
|
471
|
-
|
|
506
|
+
transfers.set(transfer, {
|
|
507
|
+
id: handle.resultId,
|
|
508
|
+
offset: 0,
|
|
509
|
+
...(write ? {} : { reclaim: { totalBytes: handle.totalBytes } }),
|
|
510
|
+
});
|
|
511
|
+
return { transfer, handle, ...(write ? {} : { reclaim: true }) };
|
|
472
512
|
}
|
|
473
513
|
}
|
|
474
514
|
return value;
|
|
475
515
|
};
|
|
516
|
+
// Slow storage may cost a run its reclaim, never its answer: all of a run's reclaim shares
|
|
517
|
+
// this much time, and once it's spent the rest of the run's entries just wait to expire.
|
|
518
|
+
let reclaimLeftMs = RECLAIM_BUDGET_MS;
|
|
519
|
+
let reclaimsLeft = RECLAIM_MAX_PER_RUN;
|
|
520
|
+
const reclaim = (id, totalBytes) => Effect.suspend(() => {
|
|
521
|
+
if (reclaimLeftMs <= 0 || reclaimsLeft <= 0)
|
|
522
|
+
return Effect.void;
|
|
523
|
+
reclaimsLeft--;
|
|
524
|
+
const started = Date.now();
|
|
525
|
+
return releaseStashedResult(registry, limits.heldResults, id, totalBytes).pipe(Effect.timeoutOrElse({ duration: Duration.millis(reclaimLeftMs), orElse: () => Effect.void }), Effect.ignore, Effect.ensuring(Effect.sync(() => {
|
|
526
|
+
reclaimLeftMs -= Date.now() - started;
|
|
527
|
+
})));
|
|
528
|
+
});
|
|
529
|
+
const dispatchDeadlineAt = limits.readDeadlineAt ?? limits.programDeadlineAt;
|
|
476
530
|
/** Budget and account for writes a writable endpoint admits. */
|
|
477
|
-
const invocationContext = (sending, timeoutMs) => ({
|
|
478
|
-
source: "execute_code",
|
|
531
|
+
const invocationContext = (sending, timeoutMs, writeTimeoutMs) => ({
|
|
532
|
+
source: limits.readProgram ? READ_PROGRAM_SOURCE : "execute_code",
|
|
479
533
|
sentSecrets,
|
|
480
534
|
timeoutMs,
|
|
535
|
+
writeTimeoutMs,
|
|
481
536
|
...(limits.routes ? { routes: limits.routes } : {}),
|
|
482
537
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
483
538
|
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
539
|
+
...(dispatchDeadlineAt !== undefined
|
|
540
|
+
? {
|
|
541
|
+
// Past the gate's time, a call's answer could not reach the program either.
|
|
542
|
+
dispatchDeadline: {
|
|
543
|
+
at: dispatchDeadlineAt,
|
|
544
|
+
refusal: (address) => ({
|
|
545
|
+
code: "timeout",
|
|
546
|
+
message: `Too little of this run's deadline remains for ${address}, so it was not sent. Return what you have and send it in a new program.`,
|
|
547
|
+
retryable: true,
|
|
548
|
+
details: { operation: address, stage: "dispatch gate" },
|
|
549
|
+
}),
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
484
553
|
unwrapResult: true,
|
|
485
554
|
readOnly: limits.readOnly,
|
|
486
555
|
processResult: authMethod((value, resolved, secrets, format, raw) => Effect.gen(function* () {
|
|
487
556
|
// Dispatch succeeded before presentation began. Cancellation of paging
|
|
488
557
|
// cannot turn this completed write into an unknown or failed write.
|
|
489
558
|
sending.settle?.("ok");
|
|
559
|
+
const write = resolved.definition.classification !== "read";
|
|
490
560
|
const page = (data, valueFormat, wireBytes) => authCall(pageProgramResult, [
|
|
491
561
|
registry,
|
|
492
562
|
baseUrl,
|
|
@@ -495,7 +565,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
495
565
|
valueFormat,
|
|
496
566
|
secrets,
|
|
497
567
|
requestScope,
|
|
568
|
+
{ max: maxHostCalls, left: maxHostCalls - hostCalls.attempted },
|
|
498
569
|
wireBytes,
|
|
570
|
+
limits.followUpRoutes,
|
|
571
|
+
limits.heldResults,
|
|
572
|
+
readBytes,
|
|
499
573
|
]);
|
|
500
574
|
const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
|
|
501
575
|
? raw
|
|
@@ -516,7 +590,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
516
590
|
const content = envelope?.content;
|
|
517
591
|
if (!Array.isArray(content) ||
|
|
518
592
|
!content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
|
|
519
|
-
return transportResult(yield* page(value, format));
|
|
593
|
+
return transportResult(yield* page(value, format), write);
|
|
520
594
|
}
|
|
521
595
|
const unavailablePresentation = () => ({
|
|
522
596
|
format: "paged",
|
|
@@ -542,7 +616,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
542
616
|
catch (error) {
|
|
543
617
|
if (!(error instanceof NativePresentationLimit))
|
|
544
618
|
throw error;
|
|
545
|
-
const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format));
|
|
619
|
+
const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format), write);
|
|
546
620
|
const notice = unavailablePresentation();
|
|
547
621
|
return result ? { value: result, native: { value: notice } } : notice;
|
|
548
622
|
}
|
|
@@ -556,7 +630,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
556
630
|
});
|
|
557
631
|
let valueResult;
|
|
558
632
|
if (native.format === "paged") {
|
|
559
|
-
valueResult = result ? transportResult(result) : undefined;
|
|
633
|
+
valueResult = result ? transportResult(result, write) : undefined;
|
|
560
634
|
if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
|
|
561
635
|
return valueResult ? { value: valueResult, native: { value: native } } : native;
|
|
562
636
|
}
|
|
@@ -566,14 +640,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
566
640
|
// absent or connector-owned blocks.
|
|
567
641
|
if (!retained) {
|
|
568
642
|
const notice = native.format === "paged" ? native : unavailablePresentation();
|
|
569
|
-
const valueResult = result ? transportResult(result) : undefined;
|
|
643
|
+
const valueResult = result ? transportResult(result, write) : undefined;
|
|
570
644
|
return valueResult ? { value: valueResult, native: { value: notice } } : notice;
|
|
571
645
|
}
|
|
572
646
|
const ref = crypto.randomUUID();
|
|
573
647
|
nativeResults.set(ref, retained);
|
|
574
648
|
return {
|
|
575
649
|
...(valueResult ? { value: valueResult } : {}),
|
|
576
|
-
native: { ref, value: transportResult(native), dataKey },
|
|
650
|
+
native: { ref, value: transportResult(native, write), dataKey },
|
|
577
651
|
};
|
|
578
652
|
})),
|
|
579
653
|
beforeWrite: (target) => Effect.sync(() => {
|
|
@@ -588,13 +662,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
588
662
|
unrecorded: true,
|
|
589
663
|
};
|
|
590
664
|
}
|
|
665
|
+
// A write is exactly what its program asked for, or it stays here. Reads
|
|
666
|
+
// keep the filtered value: sending one changes nothing it could not repeat.
|
|
667
|
+
if (sending.rewritten) {
|
|
668
|
+
return {
|
|
669
|
+
kind: "refuse",
|
|
670
|
+
error: {
|
|
671
|
+
code: "invalid_args",
|
|
672
|
+
message: `${target.connector.id}.${target.toolName} was not sent: its arguments hold a credential this request sent, and a write is never sent altered. If the value is genuine, send it as its own direct call.`,
|
|
673
|
+
retryable: false,
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// Its answer could not reach the program, so refuse now while the outcome
|
|
678
|
+
// is known. Earlier writes go out and drain past the deadline (W9).
|
|
679
|
+
if (limits.readDeadlineAt !== undefined && Date.now() >= limits.readDeadlineAt) {
|
|
680
|
+
return {
|
|
681
|
+
kind: "refuse",
|
|
682
|
+
error: {
|
|
683
|
+
code: "timeout",
|
|
684
|
+
message: `Too little of this run's deadline remains for ${target.connector.id}.${target.toolName}, so it was not sent. Return what you have and send it in a new program.`,
|
|
685
|
+
retryable: true,
|
|
686
|
+
details: { operation: `${target.connector.id}.${target.toolName}`, stage: "write gate" },
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
591
690
|
const budgeted = target.definition.classification !== "read";
|
|
592
691
|
if (budgeted && writes >= maxWrites) {
|
|
593
692
|
return {
|
|
594
693
|
kind: "refuse",
|
|
595
694
|
error: {
|
|
596
695
|
code: "budget_exceeded",
|
|
597
|
-
message: `execute_code write budget exceeded (${maxWrites} writes
|
|
696
|
+
message: `execute_code write budget exceeded (${maxWrites} writes per run); ${target.connector.id}.${target.toolName} was not sent. Check the run's writes count, then send only the rest in a new program; do not resend those that succeeded.`,
|
|
598
697
|
retryable: false,
|
|
599
698
|
},
|
|
600
699
|
};
|
|
@@ -638,12 +737,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
638
737
|
const requestedMs = normalizeTimeoutMs(timeout) ??
|
|
639
738
|
normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
|
|
640
739
|
hostCallTimeoutMs;
|
|
641
|
-
const timeoutMs =
|
|
740
|
+
const timeoutMs = readBudget(requestedMs);
|
|
741
|
+
const writeTimeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
642
742
|
// A program write settles here — unknown if the call never returned an
|
|
643
743
|
// outcome. Credentials are filtered from arguments; submitted private values are not.
|
|
644
|
-
const
|
|
744
|
+
const filtered = sentSecrets.filterCredentials(args ?? {});
|
|
745
|
+
const sending = { rewritten: filtered.changed };
|
|
645
746
|
const outcome = yield* invocation
|
|
646
|
-
.pipeline(address,
|
|
747
|
+
.pipeline(address, filtered.value, invocationContext(sending, timeoutMs, writeTimeoutMs))
|
|
647
748
|
.pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
|
|
648
749
|
if (outcome.resolved) {
|
|
649
750
|
const resolved = outcome.resolved;
|
|
@@ -679,19 +780,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
679
780
|
const discovery = (operation, read) => Effect.suspend(() => {
|
|
680
781
|
const started = Date.now();
|
|
681
782
|
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
783
|
+
// In the race itself, not behind a Promise: losing interrupts the catalog
|
|
784
|
+
// work and aborts its listing before the program hears the outcome (L3).
|
|
785
|
+
// The catalog hears the run's end as well, so its failure may arrive first.
|
|
786
|
+
const reading = Effect.suspend(read).pipe(Effect.catchDefect((err) => Effect.fail(err)), Effect.mapError((err) => hostAccessSignal?.aborted
|
|
787
|
+
? cancelled()
|
|
788
|
+
: err instanceof DiscoveryPolicyError
|
|
789
|
+
? guestFailure(err.code, err.message)
|
|
790
|
+
: err));
|
|
791
|
+
const contenders = [reading];
|
|
792
|
+
// A catalog probe may outlast the run; end it as a catchable timeout first.
|
|
793
|
+
if (limits.readDeadlineAt !== undefined)
|
|
794
|
+
contenders.push(Effect.suspend(() => {
|
|
795
|
+
const deadlineMs = readBudget(Infinity);
|
|
796
|
+
return Effect.sleep(Duration.millis(deadlineMs)).pipe(Effect.andThen(Effect.suspend(() => {
|
|
797
|
+
const elapsedMs = Date.now() - started;
|
|
798
|
+
const name = `connecta.${operation}`;
|
|
799
|
+
return Effect.fail(new InvocationFailure({
|
|
800
|
+
code: "timeout",
|
|
801
|
+
message: `Operation "${name}" timed out during catalog after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`,
|
|
802
|
+
retryable: true,
|
|
803
|
+
details: { operation: name, stage: "catalog", elapsedMs, deadlineMs },
|
|
804
|
+
}));
|
|
805
|
+
})));
|
|
806
|
+
}));
|
|
807
|
+
if (hostAccessSignal)
|
|
808
|
+
contenders.push(fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled)));
|
|
809
|
+
return (hostAccessSignal?.aborted ? Effect.fail(cancelled()) : Effect.raceAllFirst(contenders)).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
691
810
|
});
|
|
692
811
|
const meta = createMetaTools(registry, baseUrl, {
|
|
693
812
|
readOnly: limits.readOnly,
|
|
694
813
|
requestScope,
|
|
814
|
+
heldResults: limits.heldResults,
|
|
695
815
|
onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
|
|
696
816
|
});
|
|
697
817
|
const catalogDependency = (connector, tool) => {
|
|
@@ -709,63 +829,93 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
709
829
|
catalogDependency(resolved.connector.id, resolved.toolName);
|
|
710
830
|
};
|
|
711
831
|
const operations = {
|
|
712
|
-
// Uncounted: a
|
|
832
|
+
// Uncounted: a program read a result field off an unawaited host call.
|
|
713
833
|
unawaited: () => {
|
|
714
834
|
if (limits.unawaited)
|
|
715
835
|
limits.unawaited.read = true;
|
|
716
836
|
},
|
|
717
837
|
call,
|
|
718
|
-
result: (id, options, signal, onStage) =>
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
838
|
+
result: (id, options, signal, onStage) => {
|
|
839
|
+
const finished = typeof id === "string" ? transfers.get(id) : undefined;
|
|
840
|
+
if (finished?.finished) {
|
|
841
|
+
// The prelude has the whole value, so a read gives its bytes back.
|
|
842
|
+
transfers.delete(id);
|
|
843
|
+
return finished.reclaim
|
|
844
|
+
? reclaim(finished.id, finished.reclaim.totalBytes).pipe(Effect.as({}))
|
|
845
|
+
: Effect.succeed({});
|
|
846
|
+
}
|
|
847
|
+
return Effect.tryPromise({
|
|
848
|
+
try: async () => {
|
|
849
|
+
const transfer = typeof id === "string" ? transfers.get(id) : undefined;
|
|
850
|
+
const transferId = id;
|
|
851
|
+
if (transfer) {
|
|
852
|
+
id = transfer.id;
|
|
853
|
+
options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
|
|
854
|
+
}
|
|
855
|
+
else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
|
|
856
|
+
id = id.resultId;
|
|
857
|
+
}
|
|
858
|
+
if (typeof id !== "string" ||
|
|
859
|
+
(!options && options !== undefined) ||
|
|
860
|
+
(options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
|
|
861
|
+
throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? })."));
|
|
862
|
+
}
|
|
863
|
+
const args = { ...(options ?? {}) };
|
|
864
|
+
const whole = args.all === true;
|
|
865
|
+
if (args.all === false)
|
|
866
|
+
delete args.all;
|
|
867
|
+
if ((whole && Object.keys(args).length > 1) || (args.all !== undefined && !whole)) {
|
|
868
|
+
throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { all: true }) with no other options."));
|
|
869
|
+
}
|
|
870
|
+
delete args.all;
|
|
871
|
+
if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
|
|
872
|
+
(args.page !== undefined &&
|
|
873
|
+
(typeof args.page !== "number" ||
|
|
874
|
+
!Number.isSafeInteger(args.page) ||
|
|
875
|
+
args.page < 0 ||
|
|
876
|
+
args.offset !== undefined)) ||
|
|
877
|
+
(args.offset !== undefined &&
|
|
878
|
+
(typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
|
|
879
|
+
(args.maxBytes !== undefined &&
|
|
880
|
+
(typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
|
|
881
|
+
throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive."));
|
|
882
|
+
}
|
|
883
|
+
// A page must fit every shipped bridge even when the connector cap is raised.
|
|
884
|
+
args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
|
|
885
|
+
const page = await meta.readResult({ id, ...args }, {
|
|
886
|
+
signal: signal,
|
|
887
|
+
onStage: onStage,
|
|
888
|
+
});
|
|
889
|
+
if (page.isError)
|
|
890
|
+
throw new InvocationFailure((page.structuredContent?.error ?? {
|
|
891
|
+
code: "not_found",
|
|
892
|
+
message: page.content[0]?.text ?? "Result unavailable.",
|
|
893
|
+
retryable: false,
|
|
894
|
+
}));
|
|
895
|
+
if (transfer) {
|
|
896
|
+
if (page.structuredContent?.hasMore)
|
|
897
|
+
transfer.offset = page.structuredContent.nextOffset;
|
|
898
|
+
else if (transfer.reclaim)
|
|
899
|
+
transfer.finished = true;
|
|
900
|
+
else
|
|
901
|
+
transfers.delete(transferId);
|
|
902
|
+
}
|
|
903
|
+
if (whole) {
|
|
904
|
+
const totalBytes = page.structuredContent?.totalBytes;
|
|
905
|
+
if (totalBytes > readBytes) {
|
|
906
|
+
throw guestFailure("result_too_large", `This result is ${totalBytes} bytes and connecta.result(id, { all: true }) reads at most ${readBytes}. Page it with { offset } and keep only what you need from each page.`);
|
|
907
|
+
}
|
|
908
|
+
if (!page.structuredContent?.hasMore)
|
|
909
|
+
return page.structuredContent;
|
|
910
|
+
const started = `transfer:${crypto.randomUUID()}`;
|
|
911
|
+
transfers.set(started, { id, offset: page.structuredContent.nextOffset });
|
|
912
|
+
return { ...page.structuredContent, transfer: started };
|
|
913
|
+
}
|
|
914
|
+
return page.structuredContent;
|
|
915
|
+
},
|
|
916
|
+
catch: (err) => err,
|
|
917
|
+
});
|
|
918
|
+
},
|
|
769
919
|
skill: (name, _options, utilitySignal) => Effect.tryPromise({
|
|
770
920
|
try: async () => {
|
|
771
921
|
// Accept the top-level skills tool's { name } shape too: models copy it into programs.
|
|
@@ -799,7 +949,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
799
949
|
baseUrl,
|
|
800
950
|
requestScope,
|
|
801
951
|
sentSecrets,
|
|
802
|
-
timeoutMs: hostCallTimeoutMs,
|
|
952
|
+
timeoutMs: readBudget(hostCallTimeoutMs),
|
|
803
953
|
onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
|
|
804
954
|
onConnectorTime: (elapsed) => {
|
|
805
955
|
connectorMs += elapsed;
|
|
@@ -839,34 +989,40 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
839
989
|
},
|
|
840
990
|
catch: (err) => err,
|
|
841
991
|
}),
|
|
842
|
-
search: (raw) => discovery("search",
|
|
992
|
+
search: (raw) => discovery("search", () => {
|
|
843
993
|
const args = (raw ?? {});
|
|
844
|
-
|
|
994
|
+
return catalog
|
|
995
|
+
.searchEffect({
|
|
845
996
|
...args,
|
|
846
997
|
includeSchemas: args.includeSchemas ?? "typescript",
|
|
847
998
|
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
848
|
-
}, catalogDependency)
|
|
849
|
-
|
|
850
|
-
|
|
999
|
+
}, catalogDependency)
|
|
1000
|
+
.pipe(Effect.map((page) => {
|
|
1001
|
+
const result = flatSearchResult(page);
|
|
1002
|
+
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
1003
|
+
return result;
|
|
1004
|
+
}));
|
|
851
1005
|
}),
|
|
852
|
-
describe: (raw) => discovery("describe",
|
|
1006
|
+
describe: (raw) => discovery("describe", () => {
|
|
853
1007
|
const args = (raw ?? {});
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1008
|
+
return catalog.describeEffect({ ...args, format: args.format ?? "json" }).pipe(Effect.map((tools) => {
|
|
1009
|
+
const result = { tools };
|
|
1010
|
+
boundedDiscoveryText(result, 'Split the address list, omit fullDescriptions, or use format: "compact".');
|
|
1011
|
+
for (const tool of result.tools) {
|
|
1012
|
+
if (!tool.error)
|
|
1013
|
+
catalogAddressDependency(tool.address);
|
|
1014
|
+
else {
|
|
1015
|
+
const resolved = registry.resolveAddress(tool.address);
|
|
1016
|
+
if (resolved)
|
|
1017
|
+
catalogDependency(resolved.connector.id);
|
|
1018
|
+
for (const connector of tool.errorDetails?.configuredConnectors ?? [])
|
|
1019
|
+
catalogDependency(connector);
|
|
1020
|
+
for (const suggestion of tool.errorDetails?.suggestions ?? [])
|
|
1021
|
+
catalogAddressDependency(suggestion);
|
|
1022
|
+
}
|
|
867
1023
|
}
|
|
868
|
-
|
|
869
|
-
|
|
1024
|
+
return result;
|
|
1025
|
+
}));
|
|
870
1026
|
}),
|
|
871
1027
|
};
|
|
872
1028
|
// Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
|
|
@@ -898,7 +1054,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
898
1054
|
return [
|
|
899
1055
|
name,
|
|
900
1056
|
(...args) => {
|
|
901
|
-
|
|
1057
|
+
// Once sealing begins the run's secret set is final, so nothing new may start.
|
|
1058
|
+
if (budgetFailure || limits.heldResults?.accepts() === false)
|
|
902
1059
|
return stopped;
|
|
903
1060
|
// Uncounted: one per run; any further call through the bridge ends the run.
|
|
904
1061
|
if (budget === "once") {
|
|
@@ -922,26 +1079,27 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
922
1079
|
if (counted)
|
|
923
1080
|
hostCalls.admitted++;
|
|
924
1081
|
const started = Date.now();
|
|
1082
|
+
const utilityDeadlineMs = readBudget(hostCallTimeoutMs);
|
|
925
1083
|
let stage = deadline ?? "";
|
|
926
1084
|
const invoke = Effect.suspend(() => operation(...args));
|
|
927
1085
|
const guarded = deadline
|
|
928
1086
|
? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
|
|
929
1087
|
stage = current;
|
|
930
1088
|
})), {
|
|
931
|
-
timeoutMs:
|
|
1089
|
+
timeoutMs: utilityDeadlineMs,
|
|
932
1090
|
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
933
1091
|
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
934
1092
|
})
|
|
935
1093
|
: invoke;
|
|
936
1094
|
const operationEffect = activity?.recordOperation && ["search", "describe", "result", "skill"].includes(name)
|
|
937
|
-
? operationActivity(activity, registry, name, guarded)
|
|
1095
|
+
? operationActivity(activity, registry, name, guarded, undefined, limits.readProgram ? READ_PROGRAM_SOURCE : undefined)
|
|
938
1096
|
: guarded;
|
|
939
|
-
const
|
|
1097
|
+
const running = runEdge(operationEffect.pipe(Effect.catch((error) => {
|
|
940
1098
|
if (deadline && hostAccessSignal?.aborted)
|
|
941
1099
|
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
942
1100
|
if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
|
|
943
1101
|
const elapsedMs = Date.now() - started;
|
|
944
|
-
const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${
|
|
1102
|
+
const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${utilityDeadlineMs}ms).`;
|
|
945
1103
|
return typedFailure(new InvocationFailure({
|
|
946
1104
|
code: "timeout",
|
|
947
1105
|
message,
|
|
@@ -950,12 +1108,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
950
1108
|
operation: `connecta.${name}`,
|
|
951
1109
|
stage,
|
|
952
1110
|
elapsedMs,
|
|
953
|
-
deadlineMs:
|
|
1111
|
+
deadlineMs: utilityDeadlineMs,
|
|
954
1112
|
},
|
|
955
1113
|
}));
|
|
956
1114
|
}
|
|
957
1115
|
return typedFailure(error);
|
|
958
|
-
})))
|
|
1116
|
+
})));
|
|
1117
|
+
limits.heldResults?.track(running);
|
|
1118
|
+
const settled = running.then((value) => {
|
|
959
1119
|
if (counted)
|
|
960
1120
|
hostCalls.succeeded++;
|
|
961
1121
|
return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
|
|
@@ -1002,7 +1162,6 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
1002
1162
|
* waiting for one.
|
|
1003
1163
|
*/
|
|
1004
1164
|
function awaitExecutor(start, signal, options = {}) {
|
|
1005
|
-
const started = Date.now();
|
|
1006
1165
|
const contenders = [
|
|
1007
1166
|
typeof start !== "function"
|
|
1008
1167
|
? start
|
|
@@ -1029,21 +1188,34 @@ function awaitExecutor(start, signal, options = {}) {
|
|
|
1029
1188
|
contenders.push(options.terminal);
|
|
1030
1189
|
const { watchdog } = options;
|
|
1031
1190
|
if (watchdog) {
|
|
1032
|
-
contenders.push(
|
|
1191
|
+
contenders.push(untilCeiling(watchdog.ceiling).pipe(Effect.andThen(Effect.suspend(() => {
|
|
1192
|
+
const { ms: deadlineMs, startedAt } = watchdog.ceiling;
|
|
1193
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1194
|
+
// Time ran out before the executor's own deadline could: queueing
|
|
1195
|
+
// spent it, or that deadline is set past the ceiling. Nothing is wedged.
|
|
1196
|
+
if (watchdog.executorDeadlineAt !== undefined && Date.now() < watchdog.executorDeadlineAt) {
|
|
1197
|
+
return Effect.fail(new InvocationFailure({
|
|
1198
|
+
code: "timeout",
|
|
1199
|
+
message: `Operation "execute_code" timed out during response ceiling after ${elapsedMs}ms (effective deadline ${deadlineMs}ms). Return sooner, or do less in one program.`,
|
|
1200
|
+
retryable: true,
|
|
1201
|
+
details: { operation: "execute_code", stage: "response ceiling", elapsedMs, deadlineMs },
|
|
1202
|
+
}));
|
|
1203
|
+
}
|
|
1033
1204
|
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
|
|
1034
|
-
watchdogMs:
|
|
1205
|
+
watchdogMs: deadlineMs,
|
|
1035
1206
|
});
|
|
1036
|
-
const elapsedMs = Date.now() - started;
|
|
1037
1207
|
return Effect.fail(new InvocationFailure({
|
|
1038
1208
|
code: "timeout",
|
|
1039
|
-
message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${
|
|
1209
|
+
message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${deadlineMs}ms); sandbox unresponsive.`,
|
|
1040
1210
|
retryable: false,
|
|
1041
|
-
details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs
|
|
1211
|
+
details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs },
|
|
1042
1212
|
}));
|
|
1043
1213
|
}))));
|
|
1044
1214
|
}
|
|
1045
1215
|
return Effect.raceAllFirst(contenders);
|
|
1046
1216
|
}
|
|
1217
|
+
/** Waits until the ceiling; already past it, one timer turn. */
|
|
1218
|
+
const untilCeiling = (ceiling) => Effect.suspend(() => Effect.sleep(Duration.millis(Math.max(1, ceiling.at - Date.now()))));
|
|
1047
1219
|
/**
|
|
1048
1220
|
* The run's own signal, aborted however the run ends. It follows the
|
|
1049
1221
|
* caller's, and aborting it on the way out is what releases outstanding host
|
|
@@ -1072,9 +1244,11 @@ function runSignal(caller) {
|
|
|
1072
1244
|
* executor, so this request's providers and program run here, never in the
|
|
1073
1245
|
* releasing request, which workerd refuses.
|
|
1074
1246
|
*/
|
|
1075
|
-
function leased(executor, signal) {
|
|
1247
|
+
function leased(executor, signal, ceiling) {
|
|
1076
1248
|
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
|
|
1077
1249
|
late: (lease) => lease.release(),
|
|
1250
|
+
// A queue configured to outwait the client still answers it.
|
|
1251
|
+
terminal: untilCeiling(ceiling).pipe(Effect.andThen(Effect.suspend(() => Effect.fail(new ExecutorAdmissionError("executor_overloaded", `Executor admission timed out after ${Date.now() - ceiling.startedAt}ms.`))))),
|
|
1078
1252
|
}), (lease) => Effect.sync(() => lease.release()));
|
|
1079
1253
|
}
|
|
1080
1254
|
/** The execute_code handler. Exported for direct testing. */
|
|
@@ -1085,12 +1259,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1085
1259
|
};
|
|
1086
1260
|
const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
|
|
1087
1261
|
const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
|
|
1088
|
-
const
|
|
1262
|
+
const readBytes = resolveBudget(executor.maxReadBytes, PROGRAM_RESULT_READ_BYTES);
|
|
1263
|
+
const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling) => Effect.suspend(() => {
|
|
1089
1264
|
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
1265
|
+
const heldResults = new HeldResults(sentSecrets, requestOwner(requestScope).credentialReads);
|
|
1090
1266
|
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
1091
1267
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
|
|
1092
1268
|
const resultDependencies = [];
|
|
1093
|
-
const unawaited = { read: false };
|
|
1094
1269
|
const dispatchController = new AbortController();
|
|
1095
1270
|
const terminal = Deferred.makeUnsafe();
|
|
1096
1271
|
let budgetFailure;
|
|
@@ -1105,6 +1280,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1105
1280
|
// every exit, using the existing bounded cleanup and deferred tail.
|
|
1106
1281
|
yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
|
|
1107
1282
|
const signal = yield* runSignal(callerSignal);
|
|
1283
|
+
yield* startable(ceiling);
|
|
1108
1284
|
// Admission comes before provider construction: queued calls retain
|
|
1109
1285
|
// no catalogs or provider closures.
|
|
1110
1286
|
let lease;
|
|
@@ -1112,7 +1288,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1112
1288
|
lease = yield* timed((elapsed) => {
|
|
1113
1289
|
if (diagnostics)
|
|
1114
1290
|
diagnostics.admissionMs = elapsed;
|
|
1115
|
-
}, leased(executor, signal));
|
|
1291
|
+
}, leased(executor, signal, ceiling));
|
|
1116
1292
|
if ((lease.waitMs ?? 0) > 0) {
|
|
1117
1293
|
logger.debug("[connecta] execute_code admitted after queue wait", {
|
|
1118
1294
|
waitMs: lease.waitMs,
|
|
@@ -1137,19 +1313,25 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1137
1313
|
maxHostCalls: config.maxHostCalls,
|
|
1138
1314
|
hostCallTimeoutMs,
|
|
1139
1315
|
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
1140
|
-
programDeadlineAt:
|
|
1316
|
+
programDeadlineAt: ceiling.at,
|
|
1317
|
+
readDeadlineAt: readDeadlineAt(executor, ceiling),
|
|
1141
1318
|
defer: config.defer,
|
|
1142
1319
|
readOnly: config.readOnly,
|
|
1143
1320
|
maxWrites: config.maxWrites,
|
|
1144
1321
|
routes: config.routes,
|
|
1322
|
+
readProgram: config.readProgram,
|
|
1323
|
+
followUpRoutes: config.followUpRoutes,
|
|
1145
1324
|
programWrites,
|
|
1325
|
+
readBytes,
|
|
1146
1326
|
resultDependencies,
|
|
1147
1327
|
dispatchController,
|
|
1148
1328
|
unawaited,
|
|
1329
|
+
heldResults,
|
|
1149
1330
|
}, requestScope)));
|
|
1150
1331
|
if (signal.aborted) {
|
|
1151
1332
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
1152
1333
|
}
|
|
1334
|
+
yield* startable(ceiling);
|
|
1153
1335
|
const admitted = lease;
|
|
1154
1336
|
// A program write may still be on the wire when the program settles:
|
|
1155
1337
|
// close, so a write gated after this is not sent, then let the ones
|
|
@@ -1169,7 +1351,19 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1169
1351
|
interrupted: (logs) => {
|
|
1170
1352
|
executorLogs = sentSecrets.redact(logs);
|
|
1171
1353
|
},
|
|
1172
|
-
}).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, {
|
|
1354
|
+
}).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, {
|
|
1355
|
+
watchdog: {
|
|
1356
|
+
ceiling,
|
|
1357
|
+
// With no published deadline, a run that queued has not shown it is wedged.
|
|
1358
|
+
executorDeadlineAt: executor.deadlineMs !== undefined
|
|
1359
|
+
? Date.now() + executor.deadlineMs
|
|
1360
|
+
: (admitted?.waitMs ?? 0) > 0
|
|
1361
|
+
? Infinity
|
|
1362
|
+
: undefined,
|
|
1363
|
+
logger: watchdog.logger,
|
|
1364
|
+
},
|
|
1365
|
+
terminal: Deferred.await(terminal),
|
|
1366
|
+
}).pipe(Effect.ensuring(Effect.suspend(() => {
|
|
1173
1367
|
programWrites.close();
|
|
1174
1368
|
dispatchController.abort();
|
|
1175
1369
|
// Finalizers are masked by default. The request must be able
|
|
@@ -1193,6 +1387,10 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1193
1387
|
maxResultChars - emitted.textChars,
|
|
1194
1388
|
programWrites.hasSucceeded,
|
|
1195
1389
|
maxResultChars,
|
|
1390
|
+
heldResults,
|
|
1391
|
+
ceiling.answerBy,
|
|
1392
|
+
readBytes,
|
|
1393
|
+
config.followUpRoutes,
|
|
1196
1394
|
]),
|
|
1197
1395
|
};
|
|
1198
1396
|
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
@@ -1200,7 +1398,15 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1200
1398
|
// the retained log prefix. Give that report one timer turn, just as
|
|
1201
1399
|
// cancellation does in awaitExecutor; never wait on a Worker isolate.
|
|
1202
1400
|
budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
|
|
1203
|
-
|
|
1401
|
+
// Every way a run answers can carry a result id, so every one of them seals, by the
|
|
1402
|
+
// time the response must be on its way. A sealing cut short leaves entries unreadable.
|
|
1403
|
+
const sealRun = Effect.suspend(() => sealHeldResults(heldResults, registry.resultsStorage(), ceiling.answerBy, callerSignal)).pipe(Effect.tap(({ refused, refusedWrite }) => Effect.sync(() => {
|
|
1404
|
+
if (refused === 0)
|
|
1405
|
+
return;
|
|
1406
|
+
retention.note = refusedWrite ? RESULTS_REFUSED_WRITE_NOTE : RESULTS_REFUSED_NOTE;
|
|
1407
|
+
logFailure(logger, "result paging unavailable", failureRecord({}));
|
|
1408
|
+
})));
|
|
1409
|
+
const answered = Effect.flatMap(exited, (exit) => {
|
|
1204
1410
|
// A synchronous fire-and-forget burst can also settle its executor in
|
|
1205
1411
|
// this turn. The host's terminal refusal always wins over that value.
|
|
1206
1412
|
if (budgetFailure instanceof HostCallBudgetExceeded) {
|
|
@@ -1224,6 +1430,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1224
1430
|
timeoutError: cancelled,
|
|
1225
1431
|
}).pipe(Effect.catch((error) => Effect.succeed(failedRun(callerSignal?.aborted ? cancelled : error, logger, reported))));
|
|
1226
1432
|
});
|
|
1433
|
+
return Effect.tap(answered, () => sealRun);
|
|
1227
1434
|
});
|
|
1228
1435
|
return authMethod(({ code, diagnostics }, options = {}) => Effect.suspend(() => {
|
|
1229
1436
|
const requestScope = config.requestScope ?? {};
|
|
@@ -1233,6 +1440,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1233
1440
|
return Effect.fail(owner.controller.signal.reason);
|
|
1234
1441
|
const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
1235
1442
|
const programWrites = new ProgramWrites();
|
|
1443
|
+
const unawaited = { read: false };
|
|
1444
|
+
const retention = {};
|
|
1445
|
+
const ceiling = responseCeiling(config.requestStartedAt ?? Date.now(), watchdog.ms);
|
|
1236
1446
|
const response = Effect.suspend(() => {
|
|
1237
1447
|
// A code-unit count above the cap is already too large in UTF-8. Check
|
|
1238
1448
|
// that first so a huge direct-call string is never encoded in full.
|
|
@@ -1241,22 +1451,53 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1241
1451
|
const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
|
|
1242
1452
|
return Effect.succeed(failureResponse(message, { code: { code: "invalid_args", message, retryable: false } }));
|
|
1243
1453
|
}
|
|
1244
|
-
return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites);
|
|
1454
|
+
return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling);
|
|
1245
1455
|
});
|
|
1246
1456
|
return operationActivity(activity, registry, "program", response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
|
|
1247
1457
|
// Every response, including intake refusals, crosses accounting and
|
|
1248
1458
|
// exactly one envelope check at this operation boundary.
|
|
1249
|
-
const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished));
|
|
1459
|
+
const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished, sentSecrets, maxResultChars));
|
|
1250
1460
|
// Calls abandoned by the guest are failed when the run cancels them.
|
|
1251
1461
|
// Connecta attaches its own integers after the envelope check.
|
|
1252
1462
|
const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
|
|
1253
|
-
|
|
1463
|
+
const note = [
|
|
1464
|
+
finishedRunNote(result, hostCalls.attempted, programWrites.sent, unawaited.read),
|
|
1465
|
+
retention.note,
|
|
1466
|
+
]
|
|
1467
|
+
.filter(Boolean)
|
|
1468
|
+
.join(" ");
|
|
1469
|
+
result.structuredContent = {
|
|
1470
|
+
...result.structuredContent,
|
|
1471
|
+
...(note ? { note } : {}),
|
|
1472
|
+
hostCalls: counts,
|
|
1473
|
+
};
|
|
1254
1474
|
if (result.content[0]?.type === "text")
|
|
1255
1475
|
result.content[0].text = JSON.stringify(result.structuredContent);
|
|
1256
1476
|
return result;
|
|
1257
|
-
}), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }));
|
|
1477
|
+
}), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }), config.readProgram ? READ_PROGRAM_SOURCE : undefined);
|
|
1258
1478
|
}));
|
|
1259
1479
|
}
|
|
1480
|
+
const RESULTS_REFUSED_NOTE = "A result this run retained was not kept for later runs, so paging its id from a new program may fail. Rerun the read for a fresh result.";
|
|
1481
|
+
const RESULTS_REFUSED_WRITE_NOTE = `${RESULTS_REFUSED_NOTE} A write's output among them may not be retrievable: that write already ran, so do not repeat it.`;
|
|
1482
|
+
const UNAWAITED_HINT = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
|
|
1483
|
+
const SENT_NOTE = "Its writes were already sent. Do not rerun it, even with a fix: read their targets in a new program.";
|
|
1484
|
+
/**
|
|
1485
|
+
* A completed run that gave the agent nothing to read looks like nothing
|
|
1486
|
+
* happened, and a write inside it is exactly what must not run again (INV-9).
|
|
1487
|
+
* Fixed text only: attached after the envelope check, like the counters.
|
|
1488
|
+
*/
|
|
1489
|
+
function finishedRunNote(result, attempted, wrote, unawaited) {
|
|
1490
|
+
if (result.isError)
|
|
1491
|
+
return undefined;
|
|
1492
|
+
if (unawaited)
|
|
1493
|
+
return wrote ? `${UNAWAITED_HINT} ${SENT_NOTE}` : UNAWAITED_HINT;
|
|
1494
|
+
const body = result.structuredContent;
|
|
1495
|
+
if (body?.result !== undefined || body?.emitted !== undefined)
|
|
1496
|
+
return undefined;
|
|
1497
|
+
if (wrote)
|
|
1498
|
+
return `Program returned nothing. ${SENT_NOTE}`;
|
|
1499
|
+
return `Program returned nothing${attempted > 0 ? ", though its calls ran" : " and made no host calls"}. Add a return.`;
|
|
1500
|
+
}
|
|
1260
1501
|
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
|
1261
1502
|
function failedRun(err, logger, { emitted, diagnostics, program }) {
|
|
1262
1503
|
const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
|
|
@@ -1397,67 +1638,87 @@ function failureResponse(message, options) {
|
|
|
1397
1638
|
function discardedEmits(emitted) {
|
|
1398
1639
|
return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
|
|
1399
1640
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
/** A title that only respells the id ("Google Calendar" for google_calendar) adds nothing. */
|
|
1405
|
-
function titleAddsInformation(id, title) {
|
|
1406
|
-
const words = inventoryWords(title);
|
|
1407
|
-
const idWords = inventoryWords(id);
|
|
1408
|
-
return words.join("") !== idWords.join("") && words.some((word) => !idWords.includes(word));
|
|
1409
|
-
}
|
|
1641
|
+
/** Follows the inventory only when an entry shown carries the mark; `*` cannot appear in an id. */
|
|
1642
|
+
const REQUIRED_GUIDE_LEGEND = ' *Read connecta.skill("connector:<id>") before its first call.';
|
|
1643
|
+
/** Deployment configuration, normalized and capped like a title: a one-line purpose, never a paragraph. */
|
|
1644
|
+
const CONNECTOR_PURPOSE_MAX_BYTES = 57;
|
|
1410
1645
|
/**
|
|
1411
1646
|
* Every configured connector, from configuration alone. Detail degrades before
|
|
1412
|
-
* names do:
|
|
1413
|
-
* byte cap do whole entries give way to an exact omission count.
|
|
1647
|
+
* names do: purposes go first, from the end of registry order, then titles, and
|
|
1648
|
+
* only past the byte cap do whole entries give way to an exact omission count.
|
|
1649
|
+
* Search and describe name optional guides.
|
|
1414
1650
|
*/
|
|
1415
1651
|
function connectorInventory(connectors) {
|
|
1416
1652
|
const prefix = "Connectors: ";
|
|
1417
1653
|
if (connectors.length === 0)
|
|
1418
1654
|
return `${prefix}none.`;
|
|
1419
1655
|
const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1656
|
+
const guideFirst = (connector) => connectorGuide(connector) !== undefined && connectorGuideRequired(connector);
|
|
1657
|
+
const legend = (shown) => (shown.some(guideFirst) ? REQUIRED_GUIDE_LEGEND : "");
|
|
1658
|
+
const entry = (connector, titled, withPurpose = false) => {
|
|
1659
|
+
const name = guideFirst(connector) ? `${connector.id}*` : connector.id;
|
|
1660
|
+
const title = titled ? connectorInventoryTitle(connector) : undefined;
|
|
1661
|
+
const description = connector.description?.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
1662
|
+
const shown = `${connector.id} ${connector.title?.replace(/\s+/g, " ").trim() ?? ""}`;
|
|
1663
|
+
const purpose = withPurpose && description && addsInformation(shown, description)
|
|
1664
|
+
? inventoryNote(description, CONNECTOR_PURPOSE_MAX_BYTES)
|
|
1665
|
+
: undefined;
|
|
1666
|
+
const notes = [title, purpose].filter((note) => note !== undefined);
|
|
1667
|
+
return notes.length > 0 ? `${name} (${notes.join("; ")})` : name;
|
|
1431
1668
|
};
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1669
|
+
const described = (purposes) => `${prefix}${connectors.map((connector, index) => entry(connector, true, index < purposes)).join(", ")}.${legend(connectors)}`;
|
|
1670
|
+
if (fits(described(0))) {
|
|
1671
|
+
// The most entries, in registry order, whose purposes still fit.
|
|
1672
|
+
let shown = 0;
|
|
1673
|
+
let over = connectors.length + 1;
|
|
1674
|
+
while (over - shown > 1) {
|
|
1675
|
+
const middle = Math.floor((shown + over) / 2);
|
|
1676
|
+
if (fits(described(middle)))
|
|
1677
|
+
shown = middle;
|
|
1678
|
+
else
|
|
1679
|
+
over = middle;
|
|
1680
|
+
}
|
|
1681
|
+
return described(shown);
|
|
1436
1682
|
}
|
|
1437
|
-
const
|
|
1683
|
+
const untitled = `${prefix}${connectors.map((connector) => entry(connector, false)).join(", ")}.${legend(connectors)}`;
|
|
1684
|
+
if (fits(untitled))
|
|
1685
|
+
return untitled;
|
|
1686
|
+
const truncated = (shown) => {
|
|
1687
|
+
const kept = connectors.slice(0, shown);
|
|
1688
|
+
const names = kept.map((connector) => entry(connector, false)).join(", ");
|
|
1689
|
+
return `${prefix}${names}${shown > 0 ? "; " : ""}+${connectors.length - shown} more.${legend(kept)}`;
|
|
1690
|
+
};
|
|
1438
1691
|
let shown = 0;
|
|
1439
|
-
while (fits(
|
|
1692
|
+
while (fits(truncated(shown + 1)))
|
|
1440
1693
|
shown++;
|
|
1441
|
-
return
|
|
1694
|
+
return truncated(shown);
|
|
1442
1695
|
}
|
|
1443
|
-
|
|
1696
|
+
/**
|
|
1697
|
+
* Short by design: Codex carries every description in each turn's tool list and
|
|
1698
|
+
* `execute_code` already holds the full declaration. It still names the guest
|
|
1699
|
+
* calls and the connectors, so a host that loads only this tool can write a
|
|
1700
|
+
* program and tell similar connectors apart.
|
|
1701
|
+
*/
|
|
1702
|
+
const READ_PROGRAM_DESCRIPTION = (hostLimits, inventory) => `Run a read-only JavaScript program without write approval: same sandbox and connecta API as ${META_ROUTES.program}, ${hostLimits.maxHostCalls} host calls. Use it for read work at an unknown address, joins, loops and reductions. Each call that can write is refused before dispatch with its next step, and catching the refusal sends nothing. search and describe list those tools with callable: false; ${META_ROUTES.write} and ${META_ROUTES.program} run them. Use async () => { ... } with connecta.search({ query }), connecta.describe({ address }), connecta.call(address, args), connecta.result(resultId, { page }), and return the answer.
|
|
1703
|
+
${inventory}`;
|
|
1704
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES, readRoute = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may write; the host approves."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : readRoute ? `One known read: ${routes.read}. One known write: ${routes.write}. Read programs: ${READ_PROGRAM_SOURCE}. Other: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "Writable endpoint: programs may write; the host approves."}`} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}${hostLimits.runDeadlineMs === undefined ? "" : `, ${hostLimits.runDeadlineMs / 1_000}s/run`}.
|
|
1444
1705
|
${connectorInventory(connectors)}
|
|
1445
1706
|
|
|
1446
|
-
Use async () => { ... }, no arguments. No
|
|
1707
|
+
Use async () => { ... }, no arguments. No imports, require, fetch, filesystem or timers. API:
|
|
1447
1708
|
\`\`\`ts
|
|
1448
1709
|
${GUEST_API_DECLARATION}
|
|
1449
1710
|
\`\`\`
|
|
1450
|
-
search and describe return objects, not arrays. Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle
|
|
1711
|
+
search and describe return objects, not arrays.${connectorGuides ? " A row with guideRequired: read its connector guide first." : ""} Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle: follow its hint. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call; over the limit the program returns nothing, even under Promise.allSettled. Near the host-call limit, return page.nextCursor to resume next program. An oversized return carries nextAction.
|
|
1451
1712
|
|
|
1452
|
-
Never repeat writes for output. emit(result) or emit(block)
|
|
1713
|
+
${readOnly ? "" : "Never repeat writes for output. "}emit(result) or emit(block) forwards MCP blocks; authored image/audio data is base64. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; text shares the return cap. connecta.skill("usage"): repair.`;
|
|
1453
1714
|
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
1454
1715
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
1455
1716
|
// view, so they live in the description, never here.
|
|
1456
1717
|
const EXECUTE_INPUT = advertisedSchema(z.object({
|
|
1457
1718
|
code: z
|
|
1458
1719
|
.string()
|
|
1459
|
-
.describe("
|
|
1460
|
-
diagnostics: z.boolean().optional().describe("Add
|
|
1720
|
+
.describe("Source text of one zero-argument JavaScript async arrow: async () => { ...; return answer; }. Pass a string, even from a JavaScript host. Use the connecta global. At most 65,536 UTF-8 bytes."),
|
|
1721
|
+
diagnostics: z.boolean().optional().describe("Add timing and result-size summaries."),
|
|
1461
1722
|
}));
|
|
1462
1723
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
1463
1724
|
export function registerExecuteTool(server, registry, ctx) {
|
|
@@ -1475,7 +1736,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1475
1736
|
};
|
|
1476
1737
|
const connectors = registry.listConnectors();
|
|
1477
1738
|
const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
|
|
1478
|
-
const
|
|
1739
|
+
const runnerConfig = {
|
|
1479
1740
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1480
1741
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
1481
1742
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
@@ -1489,23 +1750,11 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1489
1750
|
maxWrites,
|
|
1490
1751
|
maxResultChars: ctx.maxResultChars,
|
|
1491
1752
|
requestScope: ctx.requestScope,
|
|
1753
|
+
requestStartedAt: ctx.requestStartedAt,
|
|
1492
1754
|
routes: ctx.routes,
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
...hostLimits,
|
|
1497
|
-
// connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
|
|
1498
|
-
callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1499
|
-
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
|
|
1500
|
-
inputSchema: EXECUTE_INPUT,
|
|
1501
|
-
outputSchema: EXECUTE_OUTPUT,
|
|
1502
|
-
// The client sees execute_code as a write exactly when the endpoint writes.
|
|
1503
|
-
annotations: {
|
|
1504
|
-
readOnlyHint: ctx.readOnly === true,
|
|
1505
|
-
destructiveHint: ctx.readOnly !== true,
|
|
1506
|
-
openWorldHint: true,
|
|
1507
|
-
},
|
|
1508
|
-
}, authMethod((args, extra) => Effect.gen(function* () {
|
|
1755
|
+
...(ctx.readRoute ? { followUpRoutes: routeNames("read-route") } : {}),
|
|
1756
|
+
};
|
|
1757
|
+
const programHandler = (execute, name) => (args, extra) => Effect.gen(function* () {
|
|
1509
1758
|
if (ctx.client)
|
|
1510
1759
|
bindMcpClient(extra, ctx.client, ctx.activity);
|
|
1511
1760
|
const run = authMethod(() => Effect.suspend(() => {
|
|
@@ -1529,8 +1778,35 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1529
1778
|
})));
|
|
1530
1779
|
}));
|
|
1531
1780
|
const result = yield* ctx.authElicitation
|
|
1532
|
-
? authInvoke(ctx.authElicitation, "run", [
|
|
1781
|
+
? authInvoke(ctx.authElicitation, "run", [name, args, extra, run])
|
|
1533
1782
|
: authCall(run, []);
|
|
1534
1783
|
return ctx.native ? nativeRecovery(result) : result;
|
|
1535
|
-
})
|
|
1784
|
+
});
|
|
1785
|
+
const newRunner = (config) => createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, config);
|
|
1786
|
+
server.registerTool("execute_code", {
|
|
1787
|
+
description: executeDescription(emitBudgets, {
|
|
1788
|
+
...hostLimits,
|
|
1789
|
+
// connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
|
|
1790
|
+
callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1791
|
+
...(ctx.executor.deadlineMs !== undefined
|
|
1792
|
+
? { runDeadlineMs: Math.min(ctx.executor.deadlineMs, resolveBudget(ctx.watchdogMs, EXECUTE_WATCHDOG_MS)) }
|
|
1793
|
+
: {}),
|
|
1794
|
+
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes, ctx.readRoute === true),
|
|
1795
|
+
inputSchema: EXECUTE_INPUT,
|
|
1796
|
+
outputSchema: EXECUTE_OUTPUT,
|
|
1797
|
+
// The client sees execute_code as a write exactly when the endpoint writes.
|
|
1798
|
+
annotations: {
|
|
1799
|
+
readOnlyHint: ctx.readOnly === true,
|
|
1800
|
+
destructiveHint: ctx.readOnly !== true,
|
|
1801
|
+
openWorldHint: true,
|
|
1802
|
+
},
|
|
1803
|
+
}, authMethod(programHandler(newRunner(runnerConfig), "execute_code")));
|
|
1804
|
+
if (ctx.readRoute) {
|
|
1805
|
+
server.registerTool(READ_PROGRAM_SOURCE, {
|
|
1806
|
+
description: READ_PROGRAM_DESCRIPTION(hostLimits, connectorInventory(connectors)),
|
|
1807
|
+
inputSchema: EXECUTE_INPUT,
|
|
1808
|
+
outputSchema: EXECUTE_OUTPUT,
|
|
1809
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1810
|
+
}, authMethod(programHandler(newRunner({ ...runnerConfig, readProgram: true, routes: routeNames("read-route") }), READ_PROGRAM_SOURCE)));
|
|
1811
|
+
}
|
|
1536
1812
|
}
|