@zackbart/connecta 0.40.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 +148 -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 +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- 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 +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- 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 +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- 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 +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -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 +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- 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 +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- 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/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- 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 +10 -8
- 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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { operationActivity } from "./operation-activity.js";
|
|
2
|
+
import { SkillLookupError } from "./skills.js";
|
|
3
|
+
import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
|
|
1
4
|
import { META_ROUTES, routeNames } from "./agent-routes.js";
|
|
5
|
+
import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
|
|
2
6
|
import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
|
|
3
7
|
import { nativeRecovery } from "./native-recovery.js";
|
|
4
8
|
import { GUEST_API_DECLARATION, GUEST_FUNCTIONS, UNCOUNTED_GUEST_FUNCTIONS, } from "./guest-functions.js";
|
|
@@ -8,29 +12,38 @@ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
|
|
|
8
12
|
import * as z from "zod";
|
|
9
13
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
10
14
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
11
|
-
import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
|
|
12
|
-
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";
|
|
13
17
|
import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
|
|
14
18
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
|
|
15
|
-
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
19
|
+
import { boundedEchoText, clipSerialized, ConnectorCallError, msg } from "./errors.js";
|
|
16
20
|
import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
|
|
17
21
|
import { InvocationFailure, InvocationService, timed } from "./invocation.js";
|
|
18
22
|
import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
|
|
19
23
|
import { EXECUTE_OUTPUT } from "./meta-output.js";
|
|
20
24
|
import { normalizeProgramSource } from "./program-source.js";
|
|
21
25
|
import { readResource } from "./resource-read.js";
|
|
26
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
27
|
+
import { HeldResults, sealHeldResults } from "./result-seal.js";
|
|
22
28
|
import { redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
|
|
23
29
|
import { normalizeTimeoutMs } from "./timeout.js";
|
|
24
30
|
import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
25
31
|
import { closeScope } from "./runtime/connector-scope.js";
|
|
26
32
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
27
33
|
import { executorProgram } from "./runtime/executor.js";
|
|
28
|
-
import { connectorGuide, connectorGuideRequired,
|
|
34
|
+
import { connectorGuide, connectorGuideRequired, addsInformation, connectorInventoryTitle, inventoryNote, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
|
|
29
35
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
30
36
|
const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
|
|
31
37
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
32
38
|
/** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
|
|
33
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;
|
|
34
47
|
/**
|
|
35
48
|
* The outer ceiling on one execution. It sits above both executors' own
|
|
36
49
|
* deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
|
|
@@ -38,6 +51,41 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
|
38
51
|
* never settles at all.
|
|
39
52
|
*/
|
|
40
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
|
+
});
|
|
41
89
|
/**
|
|
42
90
|
* Room to name every connector of a typical deployment. Past it, complete
|
|
43
91
|
* entries plus an exact omission count stay inside this byte cap.
|
|
@@ -342,35 +390,22 @@ function serializedDiagnosticBytes(value) {
|
|
|
342
390
|
}
|
|
343
391
|
}
|
|
344
392
|
function guestFailure(code, message, retryable = false) {
|
|
345
|
-
return new InvocationFailure(
|
|
393
|
+
return new InvocationFailure(["unavailable", "unsupported_capability", "input_required_unsupported"].includes(code)
|
|
394
|
+
? utilityRecovery(code, message, retryable)
|
|
395
|
+
: { code, message, retryable });
|
|
346
396
|
}
|
|
347
397
|
/**
|
|
348
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.
|
|
349
401
|
* Keep optional recovery whole: clipping an address or echoed args could turn
|
|
350
402
|
* recovery into a different call. Oversized metadata is omitted instead.
|
|
351
403
|
*/
|
|
352
404
|
function boundedGuestFailure(failure) {
|
|
353
|
-
const boundText = (text, maxChars) => {
|
|
354
|
-
if (JSON.stringify(text).length <= maxChars)
|
|
355
|
-
return text;
|
|
356
|
-
let low = 0;
|
|
357
|
-
let high = Math.min(text.length, maxChars);
|
|
358
|
-
while (low < high) {
|
|
359
|
-
const mid = Math.ceil((low + high) / 2);
|
|
360
|
-
if (JSON.stringify(`${text.slice(0, mid)}…`).length <= maxChars)
|
|
361
|
-
low = mid;
|
|
362
|
-
else
|
|
363
|
-
high = mid - 1;
|
|
364
|
-
}
|
|
365
|
-
// Do not manufacture a lone surrogate when clipping a Unicode message.
|
|
366
|
-
if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1]))
|
|
367
|
-
low--;
|
|
368
|
-
return `${text.slice(0, low)}…`;
|
|
369
|
-
};
|
|
370
405
|
let details = {
|
|
371
406
|
...failure.details,
|
|
372
|
-
code:
|
|
373
|
-
message:
|
|
407
|
+
code: clipSerialized(failure.details.code, 128),
|
|
408
|
+
message: clipSerialized(failure.details.message, 2_000),
|
|
374
409
|
};
|
|
375
410
|
// Keep typed metadata below the executor bridge bound.
|
|
376
411
|
if (JSON.stringify(details).length > 3_700) {
|
|
@@ -396,7 +431,7 @@ class HostCallBudgetExceeded extends InvocationFailure {
|
|
|
396
431
|
constructor(hostCalls, maxHostCalls) {
|
|
397
432
|
super({
|
|
398
433
|
code: "budget_exceeded",
|
|
399
|
-
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.`,
|
|
400
435
|
retryable: false,
|
|
401
436
|
});
|
|
402
437
|
this.hostCalls = hostCalls;
|
|
@@ -426,6 +461,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
426
461
|
readOnly: limits.readOnly,
|
|
427
462
|
// A program that just missed an address cannot call search_tools.
|
|
428
463
|
searchRoute: "connecta.search",
|
|
464
|
+
markUncallableWrites: limits.readProgram,
|
|
429
465
|
concurrency: limits.discoveryConcurrency,
|
|
430
466
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
431
467
|
defer: limits.defer,
|
|
@@ -435,6 +471,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
435
471
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
436
472
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
437
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()));
|
|
438
476
|
const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
439
477
|
let budgetFailure;
|
|
440
478
|
let emitAttempts = 0;
|
|
@@ -455,33 +493,70 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
455
493
|
const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
|
|
456
494
|
let writes = 0;
|
|
457
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;
|
|
458
499
|
const transfers = new Map();
|
|
459
500
|
const nativeResults = new Map();
|
|
460
|
-
const transportResult = (value) => {
|
|
501
|
+
const transportResult = (value, write) => {
|
|
461
502
|
if (value.format === "paged") {
|
|
462
503
|
const handle = value;
|
|
463
504
|
if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
|
|
464
505
|
const transfer = `transfer:${crypto.randomUUID()}`;
|
|
465
|
-
transfers.set(transfer, {
|
|
466
|
-
|
|
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 }) };
|
|
467
512
|
}
|
|
468
513
|
}
|
|
469
514
|
return value;
|
|
470
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;
|
|
471
530
|
/** Budget and account for writes a writable endpoint admits. */
|
|
472
|
-
const invocationContext = (sending, timeoutMs) => ({
|
|
473
|
-
source: "execute_code",
|
|
531
|
+
const invocationContext = (sending, timeoutMs, writeTimeoutMs) => ({
|
|
532
|
+
source: limits.readProgram ? READ_PROGRAM_SOURCE : "execute_code",
|
|
474
533
|
sentSecrets,
|
|
475
534
|
timeoutMs,
|
|
535
|
+
writeTimeoutMs,
|
|
476
536
|
...(limits.routes ? { routes: limits.routes } : {}),
|
|
477
537
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
478
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
|
+
: {}),
|
|
479
553
|
unwrapResult: true,
|
|
480
554
|
readOnly: limits.readOnly,
|
|
481
555
|
processResult: authMethod((value, resolved, secrets, format, raw) => Effect.gen(function* () {
|
|
482
556
|
// Dispatch succeeded before presentation began. Cancellation of paging
|
|
483
557
|
// cannot turn this completed write into an unknown or failed write.
|
|
484
558
|
sending.settle?.("ok");
|
|
559
|
+
const write = resolved.definition.classification !== "read";
|
|
485
560
|
const page = (data, valueFormat, wireBytes) => authCall(pageProgramResult, [
|
|
486
561
|
registry,
|
|
487
562
|
baseUrl,
|
|
@@ -490,7 +565,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
490
565
|
valueFormat,
|
|
491
566
|
secrets,
|
|
492
567
|
requestScope,
|
|
568
|
+
{ max: maxHostCalls, left: maxHostCalls - hostCalls.attempted },
|
|
493
569
|
wireBytes,
|
|
570
|
+
limits.followUpRoutes,
|
|
571
|
+
limits.heldResults,
|
|
572
|
+
readBytes,
|
|
494
573
|
]);
|
|
495
574
|
const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
|
|
496
575
|
? raw
|
|
@@ -511,7 +590,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
511
590
|
const content = envelope?.content;
|
|
512
591
|
if (!Array.isArray(content) ||
|
|
513
592
|
!content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
|
|
514
|
-
return transportResult(yield* page(value, format));
|
|
593
|
+
return transportResult(yield* page(value, format), write);
|
|
515
594
|
}
|
|
516
595
|
const unavailablePresentation = () => ({
|
|
517
596
|
format: "paged",
|
|
@@ -537,7 +616,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
537
616
|
catch (error) {
|
|
538
617
|
if (!(error instanceof NativePresentationLimit))
|
|
539
618
|
throw error;
|
|
540
|
-
const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format));
|
|
619
|
+
const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format), write);
|
|
541
620
|
const notice = unavailablePresentation();
|
|
542
621
|
return result ? { value: result, native: { value: notice } } : notice;
|
|
543
622
|
}
|
|
@@ -551,7 +630,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
551
630
|
});
|
|
552
631
|
let valueResult;
|
|
553
632
|
if (native.format === "paged") {
|
|
554
|
-
valueResult = result ? transportResult(result) : undefined;
|
|
633
|
+
valueResult = result ? transportResult(result, write) : undefined;
|
|
555
634
|
if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
|
|
556
635
|
return valueResult ? { value: valueResult, native: { value: native } } : native;
|
|
557
636
|
}
|
|
@@ -561,14 +640,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
561
640
|
// absent or connector-owned blocks.
|
|
562
641
|
if (!retained) {
|
|
563
642
|
const notice = native.format === "paged" ? native : unavailablePresentation();
|
|
564
|
-
const valueResult = result ? transportResult(result) : undefined;
|
|
643
|
+
const valueResult = result ? transportResult(result, write) : undefined;
|
|
565
644
|
return valueResult ? { value: valueResult, native: { value: notice } } : notice;
|
|
566
645
|
}
|
|
567
646
|
const ref = crypto.randomUUID();
|
|
568
647
|
nativeResults.set(ref, retained);
|
|
569
648
|
return {
|
|
570
649
|
...(valueResult ? { value: valueResult } : {}),
|
|
571
|
-
native: { ref, value: transportResult(native), dataKey },
|
|
650
|
+
native: { ref, value: transportResult(native, write), dataKey },
|
|
572
651
|
};
|
|
573
652
|
})),
|
|
574
653
|
beforeWrite: (target) => Effect.sync(() => {
|
|
@@ -583,13 +662,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
583
662
|
unrecorded: true,
|
|
584
663
|
};
|
|
585
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
|
+
}
|
|
586
690
|
const budgeted = target.definition.classification !== "read";
|
|
587
691
|
if (budgeted && writes >= maxWrites) {
|
|
588
692
|
return {
|
|
589
693
|
kind: "refuse",
|
|
590
694
|
error: {
|
|
591
695
|
code: "budget_exceeded",
|
|
592
|
-
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.`,
|
|
593
697
|
retryable: false,
|
|
594
698
|
},
|
|
595
699
|
};
|
|
@@ -633,12 +737,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
633
737
|
const requestedMs = normalizeTimeoutMs(timeout) ??
|
|
634
738
|
normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
|
|
635
739
|
hostCallTimeoutMs;
|
|
636
|
-
const timeoutMs =
|
|
740
|
+
const timeoutMs = readBudget(requestedMs);
|
|
741
|
+
const writeTimeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
637
742
|
// A program write settles here — unknown if the call never returned an
|
|
638
743
|
// outcome. Credentials are filtered from arguments; submitted private values are not.
|
|
639
|
-
const
|
|
744
|
+
const filtered = sentSecrets.filterCredentials(args ?? {});
|
|
745
|
+
const sending = { rewritten: filtered.changed };
|
|
640
746
|
const outcome = yield* invocation
|
|
641
|
-
.pipeline(address,
|
|
747
|
+
.pipeline(address, filtered.value, invocationContext(sending, timeoutMs, writeTimeoutMs))
|
|
642
748
|
.pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
|
|
643
749
|
if (outcome.resolved) {
|
|
644
750
|
const resolved = outcome.resolved;
|
|
@@ -674,19 +780,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
674
780
|
const discovery = (operation, read) => Effect.suspend(() => {
|
|
675
781
|
const started = Date.now();
|
|
676
782
|
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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))));
|
|
686
810
|
});
|
|
687
811
|
const meta = createMetaTools(registry, baseUrl, {
|
|
688
812
|
readOnly: limits.readOnly,
|
|
689
813
|
requestScope,
|
|
814
|
+
heldResults: limits.heldResults,
|
|
690
815
|
onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
|
|
691
816
|
});
|
|
692
817
|
const catalogDependency = (connector, tool) => {
|
|
@@ -704,63 +829,93 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
704
829
|
catalogDependency(resolved.connector.id, resolved.toolName);
|
|
705
830
|
};
|
|
706
831
|
const operations = {
|
|
707
|
-
// Uncounted: a
|
|
832
|
+
// Uncounted: a program read a result field off an unawaited host call.
|
|
708
833
|
unawaited: () => {
|
|
709
834
|
if (limits.unawaited)
|
|
710
835
|
limits.unawaited.read = true;
|
|
711
836
|
},
|
|
712
837
|
call,
|
|
713
|
-
result: (id, options, signal, onStage) =>
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
+
},
|
|
764
919
|
skill: (name, _options, utilitySignal) => Effect.tryPromise({
|
|
765
920
|
try: async () => {
|
|
766
921
|
// Accept the top-level skills tool's { name } shape too: models copy it into programs.
|
|
@@ -768,6 +923,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
768
923
|
if (typeof skillName !== "string")
|
|
769
924
|
throw guestFailure("invalid_args", 'Use connecta.skill("usage") or connecta.skill("connector:<id>") with an exact skill name; the skills tool lists names.');
|
|
770
925
|
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
926
|
+
readOnly: limits.readOnly,
|
|
771
927
|
requestScope,
|
|
772
928
|
requestSignal: utilitySignal ?? hostAccessSignal,
|
|
773
929
|
onRead: (connector, source) => limits.resultDependencies?.push({
|
|
@@ -780,7 +936,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
780
936
|
});
|
|
781
937
|
return { name: skillName, format: "text", text: await skills.text(skillName) };
|
|
782
938
|
},
|
|
783
|
-
catch: (err) => err instanceof
|
|
939
|
+
catch: (err) => err instanceof SkillLookupError
|
|
940
|
+
? new InvocationFailure(skillLookupRecovery(err.message))
|
|
941
|
+
: err instanceof ConnectorCallError
|
|
942
|
+
? guestFailure(err.code, "Skills are unavailable.", err.retryable)
|
|
943
|
+
: err,
|
|
784
944
|
}),
|
|
785
945
|
read: (uri) => Effect.suspend(() => {
|
|
786
946
|
const started = Date.now();
|
|
@@ -789,7 +949,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
789
949
|
baseUrl,
|
|
790
950
|
requestScope,
|
|
791
951
|
sentSecrets,
|
|
792
|
-
timeoutMs: hostCallTimeoutMs,
|
|
952
|
+
timeoutMs: readBudget(hostCallTimeoutMs),
|
|
793
953
|
onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
|
|
794
954
|
onConnectorTime: (elapsed) => {
|
|
795
955
|
connectorMs += elapsed;
|
|
@@ -829,34 +989,40 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
829
989
|
},
|
|
830
990
|
catch: (err) => err,
|
|
831
991
|
}),
|
|
832
|
-
search: (raw) => discovery("search",
|
|
992
|
+
search: (raw) => discovery("search", () => {
|
|
833
993
|
const args = (raw ?? {});
|
|
834
|
-
|
|
994
|
+
return catalog
|
|
995
|
+
.searchEffect({
|
|
835
996
|
...args,
|
|
836
997
|
includeSchemas: args.includeSchemas ?? "typescript",
|
|
837
998
|
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
838
|
-
}, catalogDependency)
|
|
839
|
-
|
|
840
|
-
|
|
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
|
+
}));
|
|
841
1005
|
}),
|
|
842
|
-
describe: (raw) => discovery("describe",
|
|
1006
|
+
describe: (raw) => discovery("describe", () => {
|
|
843
1007
|
const args = (raw ?? {});
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
+
}
|
|
857
1023
|
}
|
|
858
|
-
|
|
859
|
-
|
|
1024
|
+
return result;
|
|
1025
|
+
}));
|
|
860
1026
|
}),
|
|
861
1027
|
};
|
|
862
1028
|
// Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
|
|
@@ -888,7 +1054,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
888
1054
|
return [
|
|
889
1055
|
name,
|
|
890
1056
|
(...args) => {
|
|
891
|
-
|
|
1057
|
+
// Once sealing begins the run's secret set is final, so nothing new may start.
|
|
1058
|
+
if (budgetFailure || limits.heldResults?.accepts() === false)
|
|
892
1059
|
return stopped;
|
|
893
1060
|
// Uncounted: one per run; any further call through the bridge ends the run.
|
|
894
1061
|
if (budget === "once") {
|
|
@@ -912,23 +1079,27 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
912
1079
|
if (counted)
|
|
913
1080
|
hostCalls.admitted++;
|
|
914
1081
|
const started = Date.now();
|
|
1082
|
+
const utilityDeadlineMs = readBudget(hostCallTimeoutMs);
|
|
915
1083
|
let stage = deadline ?? "";
|
|
916
1084
|
const invoke = Effect.suspend(() => operation(...args));
|
|
917
1085
|
const guarded = deadline
|
|
918
1086
|
? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
|
|
919
1087
|
stage = current;
|
|
920
1088
|
})), {
|
|
921
|
-
timeoutMs:
|
|
1089
|
+
timeoutMs: utilityDeadlineMs,
|
|
922
1090
|
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
923
1091
|
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
924
1092
|
})
|
|
925
1093
|
: invoke;
|
|
926
|
-
const
|
|
1094
|
+
const operationEffect = activity?.recordOperation && ["search", "describe", "result", "skill"].includes(name)
|
|
1095
|
+
? operationActivity(activity, registry, name, guarded, undefined, limits.readProgram ? READ_PROGRAM_SOURCE : undefined)
|
|
1096
|
+
: guarded;
|
|
1097
|
+
const running = runEdge(operationEffect.pipe(Effect.catch((error) => {
|
|
927
1098
|
if (deadline && hostAccessSignal?.aborted)
|
|
928
1099
|
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
929
1100
|
if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
|
|
930
1101
|
const elapsedMs = Date.now() - started;
|
|
931
|
-
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).`;
|
|
932
1103
|
return typedFailure(new InvocationFailure({
|
|
933
1104
|
code: "timeout",
|
|
934
1105
|
message,
|
|
@@ -937,12 +1108,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
937
1108
|
operation: `connecta.${name}`,
|
|
938
1109
|
stage,
|
|
939
1110
|
elapsedMs,
|
|
940
|
-
deadlineMs:
|
|
1111
|
+
deadlineMs: utilityDeadlineMs,
|
|
941
1112
|
},
|
|
942
1113
|
}));
|
|
943
1114
|
}
|
|
944
1115
|
return typedFailure(error);
|
|
945
|
-
})))
|
|
1116
|
+
})));
|
|
1117
|
+
limits.heldResults?.track(running);
|
|
1118
|
+
const settled = running.then((value) => {
|
|
946
1119
|
if (counted)
|
|
947
1120
|
hostCalls.succeeded++;
|
|
948
1121
|
return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
|
|
@@ -989,7 +1162,6 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
989
1162
|
* waiting for one.
|
|
990
1163
|
*/
|
|
991
1164
|
function awaitExecutor(start, signal, options = {}) {
|
|
992
|
-
const started = Date.now();
|
|
993
1165
|
const contenders = [
|
|
994
1166
|
typeof start !== "function"
|
|
995
1167
|
? start
|
|
@@ -1016,21 +1188,34 @@ function awaitExecutor(start, signal, options = {}) {
|
|
|
1016
1188
|
contenders.push(options.terminal);
|
|
1017
1189
|
const { watchdog } = options;
|
|
1018
1190
|
if (watchdog) {
|
|
1019
|
-
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
|
+
}
|
|
1020
1204
|
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
|
|
1021
|
-
watchdogMs:
|
|
1205
|
+
watchdogMs: deadlineMs,
|
|
1022
1206
|
});
|
|
1023
|
-
const elapsedMs = Date.now() - started;
|
|
1024
1207
|
return Effect.fail(new InvocationFailure({
|
|
1025
1208
|
code: "timeout",
|
|
1026
|
-
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.`,
|
|
1027
1210
|
retryable: false,
|
|
1028
|
-
details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs
|
|
1211
|
+
details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs },
|
|
1029
1212
|
}));
|
|
1030
1213
|
}))));
|
|
1031
1214
|
}
|
|
1032
1215
|
return Effect.raceAllFirst(contenders);
|
|
1033
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()))));
|
|
1034
1219
|
/**
|
|
1035
1220
|
* The run's own signal, aborted however the run ends. It follows the
|
|
1036
1221
|
* caller's, and aborting it on the way out is what releases outstanding host
|
|
@@ -1059,9 +1244,11 @@ function runSignal(caller) {
|
|
|
1059
1244
|
* executor, so this request's providers and program run here, never in the
|
|
1060
1245
|
* releasing request, which workerd refuses.
|
|
1061
1246
|
*/
|
|
1062
|
-
function leased(executor, signal) {
|
|
1247
|
+
function leased(executor, signal, ceiling) {
|
|
1063
1248
|
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
|
|
1064
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.`))))),
|
|
1065
1252
|
}), (lease) => Effect.sync(() => lease.release()));
|
|
1066
1253
|
}
|
|
1067
1254
|
/** The execute_code handler. Exported for direct testing. */
|
|
@@ -1072,12 +1259,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1072
1259
|
};
|
|
1073
1260
|
const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
|
|
1074
1261
|
const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
|
|
1075
|
-
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(() => {
|
|
1076
1264
|
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
1265
|
+
const heldResults = new HeldResults(sentSecrets, requestOwner(requestScope).credentialReads);
|
|
1077
1266
|
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
1078
1267
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
|
|
1079
1268
|
const resultDependencies = [];
|
|
1080
|
-
const unawaited = { read: false };
|
|
1081
1269
|
const dispatchController = new AbortController();
|
|
1082
1270
|
const terminal = Deferred.makeUnsafe();
|
|
1083
1271
|
let budgetFailure;
|
|
@@ -1092,6 +1280,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1092
1280
|
// every exit, using the existing bounded cleanup and deferred tail.
|
|
1093
1281
|
yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
|
|
1094
1282
|
const signal = yield* runSignal(callerSignal);
|
|
1283
|
+
yield* startable(ceiling);
|
|
1095
1284
|
// Admission comes before provider construction: queued calls retain
|
|
1096
1285
|
// no catalogs or provider closures.
|
|
1097
1286
|
let lease;
|
|
@@ -1099,7 +1288,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1099
1288
|
lease = yield* timed((elapsed) => {
|
|
1100
1289
|
if (diagnostics)
|
|
1101
1290
|
diagnostics.admissionMs = elapsed;
|
|
1102
|
-
}, leased(executor, signal));
|
|
1291
|
+
}, leased(executor, signal, ceiling));
|
|
1103
1292
|
if ((lease.waitMs ?? 0) > 0) {
|
|
1104
1293
|
logger.debug("[connecta] execute_code admitted after queue wait", {
|
|
1105
1294
|
waitMs: lease.waitMs,
|
|
@@ -1124,19 +1313,25 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1124
1313
|
maxHostCalls: config.maxHostCalls,
|
|
1125
1314
|
hostCallTimeoutMs,
|
|
1126
1315
|
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
1127
|
-
programDeadlineAt:
|
|
1316
|
+
programDeadlineAt: ceiling.at,
|
|
1317
|
+
readDeadlineAt: readDeadlineAt(executor, ceiling),
|
|
1128
1318
|
defer: config.defer,
|
|
1129
1319
|
readOnly: config.readOnly,
|
|
1130
1320
|
maxWrites: config.maxWrites,
|
|
1131
1321
|
routes: config.routes,
|
|
1322
|
+
readProgram: config.readProgram,
|
|
1323
|
+
followUpRoutes: config.followUpRoutes,
|
|
1132
1324
|
programWrites,
|
|
1325
|
+
readBytes,
|
|
1133
1326
|
resultDependencies,
|
|
1134
1327
|
dispatchController,
|
|
1135
1328
|
unawaited,
|
|
1329
|
+
heldResults,
|
|
1136
1330
|
}, requestScope)));
|
|
1137
1331
|
if (signal.aborted) {
|
|
1138
1332
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
1139
1333
|
}
|
|
1334
|
+
yield* startable(ceiling);
|
|
1140
1335
|
const admitted = lease;
|
|
1141
1336
|
// A program write may still be on the wire when the program settles:
|
|
1142
1337
|
// close, so a write gated after this is not sent, then let the ones
|
|
@@ -1156,7 +1351,19 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1156
1351
|
interrupted: (logs) => {
|
|
1157
1352
|
executorLogs = sentSecrets.redact(logs);
|
|
1158
1353
|
},
|
|
1159
|
-
}).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(() => {
|
|
1160
1367
|
programWrites.close();
|
|
1161
1368
|
dispatchController.abort();
|
|
1162
1369
|
// Finalizers are masked by default. The request must be able
|
|
@@ -1180,6 +1387,10 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1180
1387
|
maxResultChars - emitted.textChars,
|
|
1181
1388
|
programWrites.hasSucceeded,
|
|
1182
1389
|
maxResultChars,
|
|
1390
|
+
heldResults,
|
|
1391
|
+
ceiling.answerBy,
|
|
1392
|
+
readBytes,
|
|
1393
|
+
config.followUpRoutes,
|
|
1183
1394
|
]),
|
|
1184
1395
|
};
|
|
1185
1396
|
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
@@ -1187,7 +1398,15 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1187
1398
|
// the retained log prefix. Give that report one timer turn, just as
|
|
1188
1399
|
// cancellation does in awaitExecutor; never wait on a Worker isolate.
|
|
1189
1400
|
budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
|
|
1190
|
-
|
|
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) => {
|
|
1191
1410
|
// A synchronous fire-and-forget burst can also settle its executor in
|
|
1192
1411
|
// this turn. The host's terminal refusal always wins over that value.
|
|
1193
1412
|
if (budgetFailure instanceof HostCallBudgetExceeded) {
|
|
@@ -1211,6 +1430,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1211
1430
|
timeoutError: cancelled,
|
|
1212
1431
|
}).pipe(Effect.catch((error) => Effect.succeed(failedRun(callerSignal?.aborted ? cancelled : error, logger, reported))));
|
|
1213
1432
|
});
|
|
1433
|
+
return Effect.tap(answered, () => sealRun);
|
|
1214
1434
|
});
|
|
1215
1435
|
return authMethod(({ code, diagnostics }, options = {}) => Effect.suspend(() => {
|
|
1216
1436
|
const requestScope = config.requestScope ?? {};
|
|
@@ -1220,6 +1440,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1220
1440
|
return Effect.fail(owner.controller.signal.reason);
|
|
1221
1441
|
const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
1222
1442
|
const programWrites = new ProgramWrites();
|
|
1443
|
+
const unawaited = { read: false };
|
|
1444
|
+
const retention = {};
|
|
1445
|
+
const ceiling = responseCeiling(config.requestStartedAt ?? Date.now(), watchdog.ms);
|
|
1223
1446
|
const response = Effect.suspend(() => {
|
|
1224
1447
|
// A code-unit count above the cap is already too large in UTF-8. Check
|
|
1225
1448
|
// that first so a huge direct-call string is never encoded in full.
|
|
@@ -1228,22 +1451,53 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1228
1451
|
const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
|
|
1229
1452
|
return Effect.succeed(failureResponse(message, { code: { code: "invalid_args", message, retryable: false } }));
|
|
1230
1453
|
}
|
|
1231
|
-
return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites);
|
|
1454
|
+
return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling);
|
|
1232
1455
|
});
|
|
1233
|
-
return response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
|
|
1456
|
+
return operationActivity(activity, registry, "program", response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
|
|
1234
1457
|
// Every response, including intake refusals, crosses accounting and
|
|
1235
1458
|
// exactly one envelope check at this operation boundary.
|
|
1236
|
-
const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished));
|
|
1459
|
+
const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished, sentSecrets, maxResultChars));
|
|
1237
1460
|
// Calls abandoned by the guest are failed when the run cancels them.
|
|
1238
1461
|
// Connecta attaches its own integers after the envelope check.
|
|
1239
1462
|
const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
|
|
1240
|
-
|
|
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
|
+
};
|
|
1241
1474
|
if (result.content[0]?.type === "text")
|
|
1242
1475
|
result.content[0].text = JSON.stringify(result.structuredContent);
|
|
1243
1476
|
return result;
|
|
1244
|
-
}), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error)));
|
|
1477
|
+
}), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }), config.readProgram ? READ_PROGRAM_SOURCE : undefined);
|
|
1245
1478
|
}));
|
|
1246
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
|
+
}
|
|
1247
1501
|
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
|
1248
1502
|
function failedRun(err, logger, { emitted, diagnostics, program }) {
|
|
1249
1503
|
const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
|
|
@@ -1384,67 +1638,87 @@ function failureResponse(message, options) {
|
|
|
1384
1638
|
function discardedEmits(emitted) {
|
|
1385
1639
|
return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
|
|
1386
1640
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
/** A title that only respells the id ("Google Calendar" for google_calendar) adds nothing. */
|
|
1392
|
-
function titleAddsInformation(id, title) {
|
|
1393
|
-
const words = inventoryWords(title);
|
|
1394
|
-
const idWords = inventoryWords(id);
|
|
1395
|
-
return words.join("") !== idWords.join("") && words.some((word) => !idWords.includes(word));
|
|
1396
|
-
}
|
|
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;
|
|
1397
1645
|
/**
|
|
1398
1646
|
* Every configured connector, from configuration alone. Detail degrades before
|
|
1399
|
-
* names do:
|
|
1400
|
-
* 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.
|
|
1401
1650
|
*/
|
|
1402
1651
|
function connectorInventory(connectors) {
|
|
1403
1652
|
const prefix = "Connectors: ";
|
|
1404
1653
|
if (connectors.length === 0)
|
|
1405
1654
|
return `${prefix}none.`;
|
|
1406
1655
|
const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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;
|
|
1418
1668
|
};
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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);
|
|
1423
1682
|
}
|
|
1424
|
-
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
|
+
};
|
|
1425
1691
|
let shown = 0;
|
|
1426
|
-
while (fits(
|
|
1692
|
+
while (fits(truncated(shown + 1)))
|
|
1427
1693
|
shown++;
|
|
1428
|
-
return
|
|
1694
|
+
return truncated(shown);
|
|
1429
1695
|
}
|
|
1430
|
-
|
|
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`}.
|
|
1431
1705
|
${connectorInventory(connectors)}
|
|
1432
1706
|
|
|
1433
|
-
Use async () => { ... }, no arguments. No
|
|
1707
|
+
Use async () => { ... }, no arguments. No imports, require, fetch, filesystem or timers. API:
|
|
1434
1708
|
\`\`\`ts
|
|
1435
1709
|
${GUEST_API_DECLARATION}
|
|
1436
1710
|
\`\`\`
|
|
1437
|
-
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.
|
|
1438
1712
|
|
|
1439
|
-
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.`;
|
|
1440
1714
|
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
1441
1715
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
1442
1716
|
// view, so they live in the description, never here.
|
|
1443
1717
|
const EXECUTE_INPUT = advertisedSchema(z.object({
|
|
1444
1718
|
code: z
|
|
1445
1719
|
.string()
|
|
1446
|
-
.describe("
|
|
1447
|
-
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."),
|
|
1448
1722
|
}));
|
|
1449
1723
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
1450
1724
|
export function registerExecuteTool(server, registry, ctx) {
|
|
@@ -1462,7 +1736,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1462
1736
|
};
|
|
1463
1737
|
const connectors = registry.listConnectors();
|
|
1464
1738
|
const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
|
|
1465
|
-
const
|
|
1739
|
+
const runnerConfig = {
|
|
1466
1740
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1467
1741
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
1468
1742
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
@@ -1476,23 +1750,11 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1476
1750
|
maxWrites,
|
|
1477
1751
|
maxResultChars: ctx.maxResultChars,
|
|
1478
1752
|
requestScope: ctx.requestScope,
|
|
1753
|
+
requestStartedAt: ctx.requestStartedAt,
|
|
1479
1754
|
routes: ctx.routes,
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
...hostLimits,
|
|
1484
|
-
// connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
|
|
1485
|
-
callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1486
|
-
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
|
|
1487
|
-
inputSchema: EXECUTE_INPUT,
|
|
1488
|
-
outputSchema: EXECUTE_OUTPUT,
|
|
1489
|
-
// The client sees execute_code as a write exactly when the endpoint writes.
|
|
1490
|
-
annotations: {
|
|
1491
|
-
readOnlyHint: ctx.readOnly === true,
|
|
1492
|
-
destructiveHint: ctx.readOnly !== true,
|
|
1493
|
-
openWorldHint: true,
|
|
1494
|
-
},
|
|
1495
|
-
}, authMethod((args, extra) => Effect.gen(function* () {
|
|
1755
|
+
...(ctx.readRoute ? { followUpRoutes: routeNames("read-route") } : {}),
|
|
1756
|
+
};
|
|
1757
|
+
const programHandler = (execute, name) => (args, extra) => Effect.gen(function* () {
|
|
1496
1758
|
if (ctx.client)
|
|
1497
1759
|
bindMcpClient(extra, ctx.client, ctx.activity);
|
|
1498
1760
|
const run = authMethod(() => Effect.suspend(() => {
|
|
@@ -1516,8 +1778,35 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1516
1778
|
})));
|
|
1517
1779
|
}));
|
|
1518
1780
|
const result = yield* ctx.authElicitation
|
|
1519
|
-
? authInvoke(ctx.authElicitation, "run", [
|
|
1781
|
+
? authInvoke(ctx.authElicitation, "run", [name, args, extra, run])
|
|
1520
1782
|
: authCall(run, []);
|
|
1521
1783
|
return ctx.native ? nativeRecovery(result) : result;
|
|
1522
|
-
})
|
|
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
|
+
}
|
|
1523
1812
|
}
|