@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
package/dist/execute.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GUEST_API_DECLARATION } from "./usage-guide.js";
|
|
2
|
+
import { bindMcpClient } from "./mcp-client-context.js";
|
|
3
|
+
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
4
|
+
import { Cause, Deferred, Duration, Effect, Exit } from "effect";
|
|
2
5
|
import { z } from "zod";
|
|
3
6
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
4
7
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
|
|
9
|
+
import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
|
|
10
|
+
import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
|
|
11
|
+
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
|
|
12
|
+
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
13
|
+
import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
|
|
14
|
+
import { InvocationFailure, InvocationService, timed } from "./invocation.js";
|
|
15
|
+
import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
|
|
16
|
+
import { EXECUTE_OUTPUT } from "./meta-output.js";
|
|
11
17
|
import { normalizeProgramSource } from "./program-source.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
18
|
+
import { readResource } from "./resource-read.js";
|
|
19
|
+
import { agentOutputOperations, redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
|
|
20
|
+
import { normalizeTimeoutMs, underAnySignal } from "./timeout.js";
|
|
21
|
+
import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
22
|
+
import { closeScope } from "./runtime/connector-scope.js";
|
|
23
|
+
import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
|
|
16
24
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
17
|
-
const EXECUTE_MAX_HOST_CALLS =
|
|
18
|
-
const EXECUTE_HOST_CALL_TIMEOUT_MS =
|
|
25
|
+
const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
|
|
26
|
+
const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
19
27
|
/** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
|
|
20
28
|
const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
21
29
|
/**
|
|
@@ -24,7 +32,7 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
|
24
32
|
* always reports its own timeout first and this fires only for one that
|
|
25
33
|
* never settles at all.
|
|
26
34
|
*/
|
|
27
|
-
const EXECUTE_WATCHDOG_MS =
|
|
35
|
+
const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
|
|
28
36
|
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
29
37
|
export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
30
38
|
/**
|
|
@@ -34,8 +42,9 @@ export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
|
34
42
|
* two or three real screenshots after base64's 4/3 inflation, well short of a
|
|
35
43
|
* file-hosting ambition (design record M5).
|
|
36
44
|
*/
|
|
37
|
-
export const EXECUTE_MAX_EMITTED_BYTES =
|
|
38
|
-
export const EXECUTE_MAX_EMITTED_BLOCKS =
|
|
45
|
+
export const EXECUTE_MAX_EMITTED_BYTES = CONFIG_DEFAULTS.execute.maxEmittedBytes;
|
|
46
|
+
export const EXECUTE_MAX_EMITTED_BLOCKS = CONFIG_DEFAULTS.execute.maxEmittedBlocks;
|
|
47
|
+
const CALL_SIGNATURE = "Use connecta.call(address, args?, { timeoutMs? }) or connecta.call({ address, args?, timeoutMs? }). address must be a canonical connector.tool string.";
|
|
39
48
|
const diagnosticsEncoder = new TextEncoder();
|
|
40
49
|
class ExecuteDiagnostics {
|
|
41
50
|
started = Date.now();
|
|
@@ -85,6 +94,15 @@ class ExecuteDiagnostics {
|
|
|
85
94
|
stats.catalogMs += outcome.timing.catalogMs;
|
|
86
95
|
stats.connectorMs += outcome.timing.connectorMs;
|
|
87
96
|
}
|
|
97
|
+
recordRead(durationMs, connectorMs, ok, value) {
|
|
98
|
+
const stats = this.stats("read");
|
|
99
|
+
stats.count++;
|
|
100
|
+
stats.failures += ok ? 0 : 1;
|
|
101
|
+
stats.durationMs += durationMs;
|
|
102
|
+
stats.connectorMs += connectorMs;
|
|
103
|
+
if (ok)
|
|
104
|
+
stats.resultBytes += serializedDiagnosticBytes(value);
|
|
105
|
+
}
|
|
88
106
|
finish() {
|
|
89
107
|
const operations = [...this.operations.values()];
|
|
90
108
|
return {
|
|
@@ -101,6 +119,10 @@ class ExecuteDiagnostics {
|
|
|
101
119
|
};
|
|
102
120
|
}
|
|
103
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Guest-authored blocks exclude pointers (INV-3). Native call blocks are
|
|
124
|
+
* retained on the host and forwarded only through request-local references.
|
|
125
|
+
*/
|
|
104
126
|
const EMIT_SHAPE_HINT = '{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
|
|
105
127
|
/**
|
|
106
128
|
* Strict M1 validation: required fields present and string-valued, nothing
|
|
@@ -130,6 +152,19 @@ function requireEmittedBlock(raw) {
|
|
|
130
152
|
if (extra.length > 0) {
|
|
131
153
|
throw guestFailure("invalid_args", `connecta.emit block carries unsupported field(s) ${extra.map((key) => JSON.stringify(key)).join(", ")}; a "${String(block.type)}" block is exactly { ${fields.join(", ")} }`);
|
|
132
154
|
}
|
|
155
|
+
if (block.type === "image") {
|
|
156
|
+
if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(block.mimeType)) {
|
|
157
|
+
throw guestFailure("invalid_args", "connecta.emit image mimeType must be image/png, image/jpeg, image/gif, or image/webp.");
|
|
158
|
+
}
|
|
159
|
+
const data = block.data;
|
|
160
|
+
// RFC 4648 alphabet, complete quads and canonical padding, including zero
|
|
161
|
+
// pad bits. Decode only the last quad; validation does not copy the image.
|
|
162
|
+
if (!data.length ||
|
|
163
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data) ||
|
|
164
|
+
btoa(atob(data.slice(-4))) !== data.slice(-4)) {
|
|
165
|
+
throw guestFailure("invalid_args", "connecta.emit image data must be nonempty, strict base64 with canonical padding.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
133
168
|
return raw;
|
|
134
169
|
}
|
|
135
170
|
export class EmitCollector {
|
|
@@ -138,38 +173,157 @@ export class EmitCollector {
|
|
|
138
173
|
diagnostics;
|
|
139
174
|
blocks = [];
|
|
140
175
|
bytes = 0;
|
|
176
|
+
textChars = 0;
|
|
141
177
|
constructor(maxBytes, maxBlocks, diagnostics) {
|
|
142
178
|
this.maxBytes = maxBytes;
|
|
143
179
|
this.maxBlocks = maxBlocks;
|
|
144
180
|
this.diagnostics = diagnostics;
|
|
145
181
|
}
|
|
146
182
|
accept(raw) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
this.acceptNative([requireEmittedBlock(raw)]);
|
|
184
|
+
}
|
|
185
|
+
/** Only the host's retained downstream result may enter this path. */
|
|
186
|
+
acceptNative(blocks) {
|
|
187
|
+
let bytes = this.bytes;
|
|
188
|
+
let textChars = this.textChars;
|
|
189
|
+
let count = this.blocks.length;
|
|
190
|
+
for (const block of blocks) {
|
|
191
|
+
if (count >= this.maxBlocks) {
|
|
192
|
+
throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - bytes} of ${this.maxBytes} serialized bytes remaining`);
|
|
193
|
+
}
|
|
194
|
+
const serialized = JSON.stringify(block);
|
|
195
|
+
const size = diagnosticsEncoder.encode(serialized).byteLength;
|
|
196
|
+
if (bytes + size > this.maxBytes) {
|
|
197
|
+
throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - bytes} of ${this.maxBytes} remaining`);
|
|
198
|
+
}
|
|
199
|
+
textChars += block.type === "text" ? serialized.length : 0;
|
|
200
|
+
// Embedded resource text shares the result cap too; binary blobs use
|
|
201
|
+
// the transport budget, just like images and audio.
|
|
202
|
+
if (block.type === "resource" && "text" in block.resource)
|
|
203
|
+
textChars += serialized.length;
|
|
204
|
+
if (textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
|
|
205
|
+
throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
|
|
206
|
+
}
|
|
207
|
+
count++;
|
|
208
|
+
bytes += size;
|
|
155
209
|
}
|
|
156
|
-
this.
|
|
157
|
-
this.bytes
|
|
210
|
+
this.textChars = textChars;
|
|
211
|
+
this.bytes = bytes;
|
|
212
|
+
this.blocks.push(...blocks);
|
|
158
213
|
this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
|
|
159
214
|
}
|
|
160
215
|
}
|
|
216
|
+
/** Bound both JSON size and object amplification before retaining native blocks. */
|
|
217
|
+
class NativePresentationLimit extends Error {
|
|
218
|
+
}
|
|
219
|
+
function boundedNativeValue(value, maxBytes, copy) {
|
|
220
|
+
let bytes = 0;
|
|
221
|
+
let nodes = 0;
|
|
222
|
+
// Empty objects cost only two JSON bytes but much more heap. Derive the
|
|
223
|
+
// allocation ceiling from the transport budget, with a fixed stack ceiling.
|
|
224
|
+
const maxNodes = Math.floor(Math.min(maxBytes, PROGRAM_RESULT_INLINE_BYTES) / 32);
|
|
225
|
+
const active = new Set();
|
|
226
|
+
const charge = (size) => {
|
|
227
|
+
bytes += size;
|
|
228
|
+
if (bytes > maxBytes)
|
|
229
|
+
throw new NativePresentationLimit();
|
|
230
|
+
};
|
|
231
|
+
const string = (text) => {
|
|
232
|
+
// Count JSON escapes and UTF-8 directly, without allocating a potentially
|
|
233
|
+
// six-times-larger escaped string or byte buffer just to reject it.
|
|
234
|
+
if (text.length > maxBytes - bytes)
|
|
235
|
+
throw new NativePresentationLimit();
|
|
236
|
+
charge(2);
|
|
237
|
+
for (let i = 0; i < text.length; i++) {
|
|
238
|
+
const unit = text.charCodeAt(i);
|
|
239
|
+
if (unit === 34 || unit === 92 || unit === 8 || unit === 9 || unit === 10 || unit === 12 || unit === 13)
|
|
240
|
+
charge(2);
|
|
241
|
+
else if (unit < 32)
|
|
242
|
+
charge(6);
|
|
243
|
+
else if (unit < 128)
|
|
244
|
+
charge(1);
|
|
245
|
+
else if (unit < 2048)
|
|
246
|
+
charge(2);
|
|
247
|
+
else if (unit >= 0xd800 &&
|
|
248
|
+
unit <= 0xdbff &&
|
|
249
|
+
text.charCodeAt(i + 1) >= 0xdc00 &&
|
|
250
|
+
text.charCodeAt(i + 1) <= 0xdfff) {
|
|
251
|
+
charge(4);
|
|
252
|
+
i++;
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
charge(unit >= 0xd800 && unit <= 0xdfff ? 6 : 3);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const visit = (item, depth) => {
|
|
259
|
+
if (++nodes > maxNodes || depth > 64)
|
|
260
|
+
throw new NativePresentationLimit();
|
|
261
|
+
if (typeof item === "string") {
|
|
262
|
+
string(item);
|
|
263
|
+
return item;
|
|
264
|
+
}
|
|
265
|
+
if (item === null || typeof item === "boolean" || typeof item === "number") {
|
|
266
|
+
charge(JSON.stringify(item).length);
|
|
267
|
+
return item;
|
|
268
|
+
}
|
|
269
|
+
if (typeof item !== "object" || active.has(item))
|
|
270
|
+
throw new NativePresentationLimit();
|
|
271
|
+
const array = Array.isArray(item);
|
|
272
|
+
if (![array ? Array.prototype : Object.prototype, null].includes(Object.getPrototypeOf(item)))
|
|
273
|
+
throw new NativePresentationLimit();
|
|
274
|
+
const jsonHook = Object.getOwnPropertyDescriptor(item, "toJSON");
|
|
275
|
+
if (jsonHook && (!("value" in jsonHook) || typeof jsonHook.value === "function"))
|
|
276
|
+
throw new NativePresentationLimit();
|
|
277
|
+
active.add(item);
|
|
278
|
+
charge(2);
|
|
279
|
+
if (array) {
|
|
280
|
+
if (item.length > maxNodes - nodes)
|
|
281
|
+
throw new NativePresentationLimit();
|
|
282
|
+
const result = copy ? [] : undefined;
|
|
283
|
+
for (let i = 0; i < item.length; i++) {
|
|
284
|
+
if (i)
|
|
285
|
+
charge(1);
|
|
286
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, String(i));
|
|
287
|
+
if (descriptor && !("value" in descriptor))
|
|
288
|
+
throw new NativePresentationLimit();
|
|
289
|
+
const child = visit(descriptor?.value ?? null, depth + 1);
|
|
290
|
+
result?.push(child);
|
|
291
|
+
}
|
|
292
|
+
active.delete(item);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
const result = copy ? {} : undefined;
|
|
296
|
+
let first = true;
|
|
297
|
+
for (const key in item) {
|
|
298
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
299
|
+
if (!descriptor?.enumerable)
|
|
300
|
+
continue;
|
|
301
|
+
if (!("value" in descriptor))
|
|
302
|
+
throw new NativePresentationLimit();
|
|
303
|
+
if (descriptor.value === undefined)
|
|
304
|
+
continue;
|
|
305
|
+
if (!first)
|
|
306
|
+
charge(1);
|
|
307
|
+
first = false;
|
|
308
|
+
string(key);
|
|
309
|
+
charge(1);
|
|
310
|
+
const child = visit(descriptor.value, depth + 1);
|
|
311
|
+
if (result)
|
|
312
|
+
Object.defineProperty(result, key, { value: child, enumerable: true, writable: true, configurable: true });
|
|
313
|
+
}
|
|
314
|
+
active.delete(item);
|
|
315
|
+
return result;
|
|
316
|
+
};
|
|
317
|
+
return { value: visit(value, 0), bytes };
|
|
318
|
+
}
|
|
161
319
|
/** A positive whole-number budget, or the default when the value is unusable. */
|
|
162
320
|
function resolveBudget(value, fallback) {
|
|
163
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 1
|
|
164
|
-
? Math.trunc(value)
|
|
165
|
-
: fallback;
|
|
321
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
|
|
166
322
|
}
|
|
167
323
|
function serializedDiagnosticBytes(value) {
|
|
168
324
|
try {
|
|
169
325
|
const text = JSON.stringify(value);
|
|
170
|
-
return text === undefined
|
|
171
|
-
? 0
|
|
172
|
-
: diagnosticsEncoder.encode(text).byteLength;
|
|
326
|
+
return text === undefined ? 0 : diagnosticsEncoder.encode(text).byteLength;
|
|
173
327
|
}
|
|
174
328
|
catch {
|
|
175
329
|
// The executor's normal result guard owns the error. Diagnostics must
|
|
@@ -180,16 +334,8 @@ function serializedDiagnosticBytes(value) {
|
|
|
180
334
|
function guestFailure(code, message, retryable = false) {
|
|
181
335
|
return new InvocationFailure({ code, message, retryable });
|
|
182
336
|
}
|
|
183
|
-
const GUEST_FAILURE_FRAME = "\u001econnecta-error:";
|
|
184
|
-
const guestFailureFrames = new WeakMap();
|
|
185
|
-
/** Per-execution secret: guest prose cannot collide with this host frame. */
|
|
186
|
-
function guestFailureSecret() {
|
|
187
|
-
const words = new Uint32Array(4);
|
|
188
|
-
crypto.getRandomValues(words);
|
|
189
|
-
return Array.from(words, (word) => word.toString(16).padStart(8, "0")).join("");
|
|
190
|
-
}
|
|
191
337
|
/**
|
|
192
|
-
* E1/X11: bound
|
|
338
|
+
* E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
|
|
193
339
|
* Keep optional recovery whole: clipping an address or echoed args could turn
|
|
194
340
|
* recovery into a different call. Oversized metadata is omitted instead.
|
|
195
341
|
*/
|
|
@@ -216,67 +362,17 @@ function boundedGuestFailure(failure) {
|
|
|
216
362
|
code: boundText(failure.details.code, 128),
|
|
217
363
|
message: boundText(failure.details.message, 2_000),
|
|
218
364
|
};
|
|
219
|
-
//
|
|
220
|
-
// 4,000-character error bound, with room for bridge-added context.
|
|
365
|
+
// Keep typed metadata below the executor bridge bound.
|
|
221
366
|
if (JSON.stringify(details).length > 3_700) {
|
|
222
367
|
details = {
|
|
223
368
|
code: details.code,
|
|
224
369
|
message: details.message,
|
|
225
370
|
retryable: details.retryable,
|
|
226
|
-
...(details.retryAfterMs !== undefined
|
|
227
|
-
? { retryAfterMs: details.retryAfterMs }
|
|
228
|
-
: {}),
|
|
371
|
+
...(details.retryAfterMs !== undefined ? { retryAfterMs: details.retryAfterMs } : {}),
|
|
229
372
|
};
|
|
230
373
|
}
|
|
231
374
|
return new InvocationFailure(details);
|
|
232
375
|
}
|
|
233
|
-
function framedGuestFailure(secret, failure) {
|
|
234
|
-
const framed = new InvocationFailure(failure.details);
|
|
235
|
-
framed.message =
|
|
236
|
-
`${GUEST_FAILURE_FRAME}${secret}:${JSON.stringify(failure.details)}`;
|
|
237
|
-
guestFailureFrames.set(failure, framed.message);
|
|
238
|
-
return framed;
|
|
239
|
-
}
|
|
240
|
-
/** Rebuild host failures as guest errors without exposing the private frame. */
|
|
241
|
-
function guestErrorPrelude(failureSecret) {
|
|
242
|
-
return `((failurePrefix) => {
|
|
243
|
-
const NativeError = globalThis.Error;
|
|
244
|
-
const startsWith = Function.prototype.call.bind(String.prototype.startsWith);
|
|
245
|
-
const slice = Function.prototype.call.bind(String.prototype.slice);
|
|
246
|
-
const parse = JSON.parse;
|
|
247
|
-
const freeze = Object.freeze;
|
|
248
|
-
const defineProperties = Object.defineProperties;
|
|
249
|
-
const construct = Reflect.construct;
|
|
250
|
-
function ConnectaError(message, options) {
|
|
251
|
-
let details;
|
|
252
|
-
if (typeof message === "string" && startsWith(message, failurePrefix)) {
|
|
253
|
-
try { details = parse(slice(message, failurePrefix.length)); }
|
|
254
|
-
catch { message = "Invalid host failure frame."; }
|
|
255
|
-
}
|
|
256
|
-
const error = construct(
|
|
257
|
-
NativeError,
|
|
258
|
-
options === undefined ? [details ? details.message : message] : [details ? details.message : message, options],
|
|
259
|
-
new.target || NativeError
|
|
260
|
-
);
|
|
261
|
-
if (details) {
|
|
262
|
-
freeze(details);
|
|
263
|
-
defineProperties(error, {
|
|
264
|
-
code: { value: details.code, enumerable: true },
|
|
265
|
-
retryable: { value: details.retryable, enumerable: true },
|
|
266
|
-
details: { value: details, enumerable: true }
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
return error;
|
|
270
|
-
}
|
|
271
|
-
ConnectaError.prototype = NativeError.prototype;
|
|
272
|
-
Object.setPrototypeOf(ConnectaError, NativeError);
|
|
273
|
-
Object.defineProperty(globalThis, "Error", {
|
|
274
|
-
value: ConnectaError,
|
|
275
|
-
writable: false,
|
|
276
|
-
configurable: false
|
|
277
|
-
});
|
|
278
|
-
})(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
|
|
279
|
-
}
|
|
280
376
|
/**
|
|
281
377
|
* Expose one host provider and typed guest errors. Catalogs load only when
|
|
282
378
|
* the program calls a tool or asks search/describe.
|
|
@@ -284,58 +380,172 @@ function guestErrorPrelude(failureSecret) {
|
|
|
284
380
|
export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
|
|
285
381
|
return [sandboxProvider(registry, baseUrl, activity, limits)];
|
|
286
382
|
}
|
|
383
|
+
/** Numeric accounting only; no addresses, arguments, or results. */
|
|
384
|
+
class HostCallBudgetExceeded extends InvocationFailure {
|
|
385
|
+
hostCalls;
|
|
386
|
+
constructor(hostCalls, maxHostCalls) {
|
|
387
|
+
super({
|
|
388
|
+
code: "budget_exceeded",
|
|
389
|
+
message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
|
|
390
|
+
retryable: false,
|
|
391
|
+
});
|
|
392
|
+
this.hostCalls = hostCalls;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
287
395
|
/**
|
|
288
396
|
* The `connecta` provider for one execution.
|
|
289
397
|
*
|
|
290
398
|
* Building it is synchronous and loads nothing. Each function is a Promise
|
|
291
399
|
* edge the executor awaits, and behind it one host call is one fiber: spend
|
|
292
|
-
* the budget, do the operation, and
|
|
293
|
-
*
|
|
294
|
-
* but the budget counters and this request's catalog
|
|
295
|
-
*
|
|
400
|
+
* the budget, do the operation, and retain a typed failure for the
|
|
401
|
+
* executor bridge to rebuild inside the guest. Nothing is shared between those fibers
|
|
402
|
+
* but the budget counters and this request's catalog. Budget exhaustion ends
|
|
403
|
+
* the whole run, including calls the program never awaits.
|
|
296
404
|
*/
|
|
297
|
-
function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
405
|
+
function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {}) {
|
|
406
|
+
const sentSecrets = sentSecretsForRequest(requestScope, limits.sentSecrets);
|
|
298
407
|
// All host calls made by one execute_code invocation share a downstream
|
|
299
408
|
// connection, while a later invocation receives a fresh request scope.
|
|
300
|
-
const
|
|
409
|
+
const hostAccessSignal = limits.dispatchController
|
|
410
|
+
? AbortSignal.any([limits.dispatchController.signal, ...(limits.signal ? [limits.signal] : [])])
|
|
411
|
+
: limits.signal;
|
|
301
412
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
302
413
|
requestScope,
|
|
414
|
+
requestSignal: hostAccessSignal,
|
|
303
415
|
// A program that just missed an address cannot call search_tools.
|
|
304
416
|
searchRoute: "connecta.search",
|
|
305
417
|
concurrency: limits.discoveryConcurrency,
|
|
306
418
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
307
419
|
defer: limits.defer,
|
|
308
|
-
approval: limits.approval,
|
|
309
420
|
});
|
|
310
421
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
311
422
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
312
423
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
313
|
-
const
|
|
314
|
-
const {
|
|
315
|
-
let
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
424
|
+
const { signal, diagnostics, programWrites } = limits;
|
|
425
|
+
const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
426
|
+
let budgetFailure;
|
|
427
|
+
let emitAttempts = 0;
|
|
428
|
+
const maxEmitAttempts = Math.max(EXECUTE_MAX_EMITTED_BLOCKS, limits.emitCollector?.maxBlocks ?? EXECUTE_MAX_EMITTED_BLOCKS) * 4;
|
|
429
|
+
// A rejected bridge promise is catchable in both sandboxes. End the host
|
|
430
|
+
// run instead and leave that bridge pending: an awaiting guest cannot catch
|
|
431
|
+
// the refusal and loop. Later calls, including emit, lose host access too.
|
|
432
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
433
|
+
const ended = () => reject(new Error("The host run ended."));
|
|
434
|
+
if (signal?.aborted)
|
|
435
|
+
ended();
|
|
436
|
+
else
|
|
437
|
+
signal?.addEventListener("abort", ended, { once: true });
|
|
438
|
+
});
|
|
439
|
+
// Stay pending until the lease has disposed the sandbox, then settle the
|
|
440
|
+
// abandoned host RPC waits without retaining request resources forever.
|
|
441
|
+
stopped.catch(() => { });
|
|
322
442
|
const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
|
|
323
443
|
let writes = 0;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
444
|
+
// These are transfer cursors only. All result bytes live in the existing stash.
|
|
445
|
+
const transfers = new Map();
|
|
446
|
+
const nativeResults = new Map();
|
|
447
|
+
const transportResult = (value) => {
|
|
448
|
+
if (value.format === "paged") {
|
|
449
|
+
const handle = value;
|
|
450
|
+
if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
|
|
451
|
+
const transfer = `transfer:${crypto.randomUUID()}`;
|
|
452
|
+
transfers.set(transfer, { id: handle.resultId, offset: 0 });
|
|
453
|
+
return { transfer, handle };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return value;
|
|
457
|
+
};
|
|
458
|
+
/** Budget and account for writes admitted by the pool trust decision. */
|
|
459
|
+
const invocationContext = (sending, timeoutMs) => ({
|
|
332
460
|
source: "execute_code",
|
|
333
|
-
|
|
461
|
+
sentSecrets,
|
|
462
|
+
timeoutMs,
|
|
334
463
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
464
|
+
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
335
465
|
unwrapResult: true,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
466
|
+
trust: limits.trust,
|
|
467
|
+
processResult: async (value, resolved, secrets, format, raw) => {
|
|
468
|
+
const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, limits.trust, requestScope, wireBytes);
|
|
469
|
+
const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
|
|
470
|
+
? raw
|
|
471
|
+
: undefined;
|
|
472
|
+
const dataKey = envelope
|
|
473
|
+
? "toolResult" in envelope
|
|
474
|
+
? "toolResult"
|
|
475
|
+
: envelope.structuredContent !== undefined
|
|
476
|
+
? "structuredContent"
|
|
477
|
+
: undefined
|
|
478
|
+
: undefined;
|
|
479
|
+
// Preserve #788 before even inspecting presentation. Paging serializes
|
|
480
|
+
// the structured value synchronously; no native metadata is visited.
|
|
481
|
+
if (dataKey !== undefined &&
|
|
482
|
+
diagnosticsEncoder.encode(JSON.stringify(value)).byteLength > PROGRAM_RESULT_INLINE_BYTES) {
|
|
483
|
+
return page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
|
|
484
|
+
}
|
|
485
|
+
const content = envelope?.content;
|
|
486
|
+
if (!Array.isArray(content) ||
|
|
487
|
+
!content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
|
|
488
|
+
return transportResult(await page(value, format));
|
|
489
|
+
}
|
|
490
|
+
const unavailablePresentation = () => ({
|
|
491
|
+
format: "paged",
|
|
492
|
+
valueFormat: "json",
|
|
493
|
+
totalBytes: 0,
|
|
494
|
+
truncated: true,
|
|
495
|
+
hint: "Paging is unavailable. Native presentation exceeds byte, node or depth limits, or is not acyclic JSON. Reduce the downstream presentation before forwarding it.",
|
|
496
|
+
});
|
|
497
|
+
let snapshot;
|
|
498
|
+
let retained;
|
|
499
|
+
try {
|
|
500
|
+
// Oversized but bounded envelopes may still use the existing stash.
|
|
501
|
+
// Measure without copying them; only inline native blocks are retained.
|
|
502
|
+
const measured = boundedNativeValue(envelope, Math.max(EXECUTE_MAX_EMITTED_BYTES, limits.emitCollector?.maxBytes ?? 0), false);
|
|
503
|
+
if (measured.bytes <= PROGRAM_RESULT_INLINE_BYTES) {
|
|
504
|
+
retained = boundedNativeValue(content, PROGRAM_RESULT_INLINE_BYTES, true).value;
|
|
505
|
+
snapshot = { ...envelope, content: retained };
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
snapshot = envelope;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (!(error instanceof NativePresentationLimit))
|
|
513
|
+
throw error;
|
|
514
|
+
const result = dataKey === undefined ? undefined : transportResult(await page(value, format));
|
|
515
|
+
const notice = unavailablePresentation();
|
|
516
|
+
return result ? { value: result, native: { value: notice } } : notice;
|
|
517
|
+
}
|
|
518
|
+
// Snapshot blocks before either stash operation yields. Start both
|
|
519
|
+
// serializations now so an oversized presentation's pages also own the
|
|
520
|
+
// original bytes even if the structured-data stash pauses.
|
|
521
|
+
const resultPending = dataKey === undefined ? undefined : page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
|
|
522
|
+
const nativePending = page(snapshot, "json");
|
|
523
|
+
const result = await resultPending;
|
|
524
|
+
const native = await nativePending;
|
|
525
|
+
let valueResult;
|
|
526
|
+
if (native.format === "paged") {
|
|
527
|
+
valueResult = result ? transportResult(result) : undefined;
|
|
528
|
+
if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
|
|
529
|
+
return valueResult ? { value: valueResult, native: { value: native } } : native;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Redaction can shrink an oversized original below the inline cap. It
|
|
533
|
+
// still has no retained snapshot: keep its handle, never mint a ref to
|
|
534
|
+
// absent or connector-owned blocks.
|
|
535
|
+
if (!retained) {
|
|
536
|
+
const notice = native.format === "paged" ? native : unavailablePresentation();
|
|
537
|
+
const valueResult = result ? transportResult(result) : undefined;
|
|
538
|
+
return valueResult ? { value: valueResult, native: { value: notice } } : notice;
|
|
539
|
+
}
|
|
540
|
+
const ref = crypto.randomUUID();
|
|
541
|
+
nativeResults.set(ref, retained);
|
|
542
|
+
return {
|
|
543
|
+
...(valueResult ? { value: valueResult } : {}),
|
|
544
|
+
native: { ref, value: transportResult(native), dataKey },
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
beforeWrite: (target) => Effect.sync(() => {
|
|
548
|
+
if (programWrites?.isClosed) {
|
|
339
549
|
return {
|
|
340
550
|
kind: "refuse",
|
|
341
551
|
error: {
|
|
@@ -357,24 +567,75 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
357
567
|
};
|
|
358
568
|
}
|
|
359
569
|
writes++;
|
|
360
|
-
if (
|
|
361
|
-
sending.settle =
|
|
570
|
+
if (programWrites)
|
|
571
|
+
sending.settle = programWrites.begin();
|
|
362
572
|
return { kind: "dispatch" };
|
|
363
573
|
}),
|
|
364
574
|
});
|
|
365
575
|
// A call brings its own cancellation: the invocation pipeline reads the
|
|
366
576
|
// run's signal, refuses a call that starts after it, and records the
|
|
367
577
|
// cancelled attempt in activity like any other outcome.
|
|
368
|
-
const call = (address, args) => Effect.gen(function* () {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
578
|
+
const call = (address, args, options) => Effect.gen(function* () {
|
|
579
|
+
let timeout;
|
|
580
|
+
if (address !== null && typeof address === "object" && !Array.isArray(address)) {
|
|
581
|
+
const request = address;
|
|
582
|
+
if (args !== undefined ||
|
|
583
|
+
options !== undefined ||
|
|
584
|
+
Object.keys(request).some((key) => !["address", "args", "timeoutMs"].includes(key))) {
|
|
585
|
+
return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
|
|
586
|
+
}
|
|
587
|
+
address = request.address;
|
|
588
|
+
args = request.args;
|
|
589
|
+
timeout = request.timeoutMs;
|
|
590
|
+
}
|
|
591
|
+
else if (options !== undefined) {
|
|
592
|
+
if (!options ||
|
|
593
|
+
typeof options !== "object" ||
|
|
594
|
+
Array.isArray(options) ||
|
|
595
|
+
Object.keys(options).some((key) => key !== "timeoutMs")) {
|
|
596
|
+
return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
|
|
597
|
+
}
|
|
598
|
+
timeout = options.timeoutMs;
|
|
599
|
+
}
|
|
600
|
+
if (typeof address !== "string" ||
|
|
601
|
+
(timeout !== undefined && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 1))) {
|
|
602
|
+
return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
|
|
603
|
+
}
|
|
604
|
+
const requestedMs = normalizeTimeoutMs(timeout) ??
|
|
605
|
+
normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
|
|
606
|
+
hostCallTimeoutMs;
|
|
607
|
+
const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
608
|
+
// A trusted-pool write settles here — unknown if the call never returned an
|
|
609
|
+
// outcome. Credentials are filtered from arguments; submitted private values are not.
|
|
372
610
|
const sending = {};
|
|
373
|
-
const outcome = yield* invocation
|
|
611
|
+
const outcome = yield* invocation
|
|
612
|
+
.pipeline(address, sentSecrets.redactCredentials(args ?? {}), invocationContext(sending, timeoutMs))
|
|
613
|
+
.pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
|
|
614
|
+
if (outcome.resolved) {
|
|
615
|
+
const resolved = outcome.resolved;
|
|
616
|
+
limits.resultDependencies?.push({
|
|
617
|
+
connector: resolved.connector.id,
|
|
618
|
+
tool: resolved.definition.name,
|
|
619
|
+
classification: outcome.dispatched && resolved.definition.classification !== "read" ? "write" : "read",
|
|
620
|
+
// A refusal exposes catalog metadata, not a completed write's result.
|
|
621
|
+
...(!outcome.dispatched ? { access: "catalog" } : {}),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// Resolution failures can expose a catalog error or connector metadata
|
|
626
|
+
// without ever finding a tool. Such bytes depend on connector visibility,
|
|
627
|
+
// not on the existence of the requested (possibly unknown) tool.
|
|
628
|
+
const target = registry.resolveAddress(address);
|
|
629
|
+
if (target)
|
|
630
|
+
catalogDependency(target.connector.id);
|
|
631
|
+
if (!outcome.ok)
|
|
632
|
+
for (const connector of outcome.error.configuredConnectors ?? [])
|
|
633
|
+
catalogDependency(connector);
|
|
634
|
+
}
|
|
374
635
|
diagnostics?.recordCall(outcome);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
636
|
+
if (!outcome.ok)
|
|
637
|
+
return yield* Effect.fail(new InvocationFailure(outcome.error));
|
|
638
|
+
return outcome.value;
|
|
378
639
|
});
|
|
379
640
|
// Discovery gets the same treatment from here (L2): once the run has
|
|
380
641
|
// ended no search or describe starts, and one still in flight fails
|
|
@@ -386,21 +647,117 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
386
647
|
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
387
648
|
const reading = Effect.tryPromise({
|
|
388
649
|
try: read,
|
|
389
|
-
catch: (err) => err instanceof DiscoveryPolicyError
|
|
390
|
-
? guestFailure(err.code, err.message)
|
|
391
|
-
: err,
|
|
650
|
+
catch: (err) => (err instanceof DiscoveryPolicyError ? guestFailure(err.code, err.message) : err),
|
|
392
651
|
});
|
|
393
|
-
return
|
|
652
|
+
return (!hostAccessSignal
|
|
394
653
|
? reading
|
|
395
|
-
:
|
|
654
|
+
: hostAccessSignal.aborted
|
|
396
655
|
? Effect.fail(cancelled())
|
|
397
|
-
: Effect.raceAllFirst([
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
656
|
+
: Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
657
|
+
});
|
|
658
|
+
const meta = createMetaTools(registry, baseUrl, {
|
|
659
|
+
trust: limits.trust,
|
|
660
|
+
requestScope,
|
|
661
|
+
onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
|
|
401
662
|
});
|
|
663
|
+
const catalogDependency = (connector, tool) => {
|
|
664
|
+
if (registry.getConnector(connector))
|
|
665
|
+
limits.resultDependencies?.push({
|
|
666
|
+
connector,
|
|
667
|
+
...(tool ? { tool } : {}),
|
|
668
|
+
classification: "read",
|
|
669
|
+
access: "catalog",
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
const catalogAddressDependency = (address) => {
|
|
673
|
+
const resolved = registry.resolveAddress(address);
|
|
674
|
+
if (resolved)
|
|
675
|
+
catalogDependency(resolved.connector.id, resolved.toolName);
|
|
676
|
+
};
|
|
402
677
|
const operations = {
|
|
403
678
|
call,
|
|
679
|
+
result: (id, options, signal) => Effect.tryPromise({
|
|
680
|
+
try: async () => {
|
|
681
|
+
const transfer = typeof id === "string" ? transfers.get(id) : undefined;
|
|
682
|
+
const transferId = id;
|
|
683
|
+
if (transfer) {
|
|
684
|
+
id = transfer.id;
|
|
685
|
+
options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
|
|
686
|
+
}
|
|
687
|
+
else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
|
|
688
|
+
id = id.resultId;
|
|
689
|
+
}
|
|
690
|
+
if (typeof id !== "string" ||
|
|
691
|
+
(!options && options !== undefined) ||
|
|
692
|
+
(options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
|
|
693
|
+
throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }).");
|
|
694
|
+
}
|
|
695
|
+
const args = { ...(options ?? {}) };
|
|
696
|
+
if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
|
|
697
|
+
(args.page !== undefined &&
|
|
698
|
+
(typeof args.page !== "number" ||
|
|
699
|
+
!Number.isSafeInteger(args.page) ||
|
|
700
|
+
args.page < 0 ||
|
|
701
|
+
args.offset !== undefined)) ||
|
|
702
|
+
(args.offset !== undefined &&
|
|
703
|
+
(typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
|
|
704
|
+
(args.maxBytes !== undefined &&
|
|
705
|
+
(typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
|
|
706
|
+
throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive.");
|
|
707
|
+
}
|
|
708
|
+
// A page must fit every shipped bridge even when the connector cap is raised.
|
|
709
|
+
args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
|
|
710
|
+
const page = await meta.readResult({ id, ...args }, { signal: signal });
|
|
711
|
+
if (page.isError)
|
|
712
|
+
throw new InvocationFailure((page.structuredContent?.error ?? {
|
|
713
|
+
code: "not_found",
|
|
714
|
+
message: page.content[0]?.text ?? "Result unavailable.",
|
|
715
|
+
retryable: false,
|
|
716
|
+
}));
|
|
717
|
+
if (transfer) {
|
|
718
|
+
if (page.structuredContent?.hasMore)
|
|
719
|
+
transfer.offset = page.structuredContent.nextOffset;
|
|
720
|
+
else
|
|
721
|
+
transfers.delete(transferId);
|
|
722
|
+
}
|
|
723
|
+
return page.structuredContent;
|
|
724
|
+
},
|
|
725
|
+
catch: (err) => err,
|
|
726
|
+
}),
|
|
727
|
+
skill: (name, _options, utilitySignal) => Effect.tryPromise({
|
|
728
|
+
try: async () => {
|
|
729
|
+
if (typeof name !== "string")
|
|
730
|
+
throw guestFailure("invalid_args", "Use connecta.skill(name) with an exact skill name.");
|
|
731
|
+
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
732
|
+
requestScope,
|
|
733
|
+
requestSignal: utilitySignal ?? hostAccessSignal,
|
|
734
|
+
onRead: (connector, source) => limits.resultDependencies?.push({
|
|
735
|
+
connector: connector.id,
|
|
736
|
+
classification: "read",
|
|
737
|
+
...(source === "local" ? { access: "catalog" } : {}),
|
|
738
|
+
}),
|
|
739
|
+
probeTimeoutMs: limits.probeTimeoutMs,
|
|
740
|
+
defer: limits.defer,
|
|
741
|
+
});
|
|
742
|
+
return { name, format: "text", text: await skills.text(name) };
|
|
743
|
+
},
|
|
744
|
+
catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
|
|
745
|
+
}),
|
|
746
|
+
read: (uri) => Effect.suspend(() => {
|
|
747
|
+
const started = Date.now();
|
|
748
|
+
let connectorMs = 0;
|
|
749
|
+
return readResource(registry, uri, {
|
|
750
|
+
baseUrl,
|
|
751
|
+
requestScope,
|
|
752
|
+
sentSecrets,
|
|
753
|
+
timeoutMs: hostCallTimeoutMs,
|
|
754
|
+
onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
|
|
755
|
+
onConnectorTime: (elapsed) => {
|
|
756
|
+
connectorMs += elapsed;
|
|
757
|
+
},
|
|
758
|
+
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
759
|
+
}).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordRead(Date.now() - started, connectorMs, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
760
|
+
}),
|
|
404
761
|
// Emission is a provider function, never an ExecuteResult field —
|
|
405
762
|
// that is what keeps the Executor contract untouched and parity
|
|
406
763
|
// structural (M8). It spends no host-call budget (M7); its own
|
|
@@ -410,7 +767,26 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
410
767
|
if (!limits.emitCollector) {
|
|
411
768
|
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
412
769
|
}
|
|
413
|
-
|
|
770
|
+
if (block !== null && typeof block === "object" && "contentResult" in block) {
|
|
771
|
+
throw guestFailure("result_too_large", "Native content is not inline; page result.contentResult with connecta.result and reduce it before emitting.");
|
|
772
|
+
}
|
|
773
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block) && "ref" in block) {
|
|
774
|
+
const reference = block;
|
|
775
|
+
const native = typeof reference.ref === "string" ? nativeResults.get(reference.ref) : undefined;
|
|
776
|
+
if (!native ||
|
|
777
|
+
Object.keys(reference).some((key) => !["ref", "index"].includes(key)) ||
|
|
778
|
+
(reference.index !== undefined &&
|
|
779
|
+
(typeof reference.index !== "number" ||
|
|
780
|
+
!Number.isSafeInteger(reference.index) ||
|
|
781
|
+
reference.index < 0 ||
|
|
782
|
+
reference.index >= native.length))) {
|
|
783
|
+
throw guestFailure("invalid_args", "connecta.emit requires a native result from this program's connecta.call.");
|
|
784
|
+
}
|
|
785
|
+
const blocks = reference.index === undefined ? native : [native[reference.index]];
|
|
786
|
+
limits.emitCollector.acceptNative(sentSecrets.redact(blocks));
|
|
787
|
+
}
|
|
788
|
+
else
|
|
789
|
+
limits.emitCollector.accept(sentSecrets.redact(block));
|
|
414
790
|
},
|
|
415
791
|
catch: (err) => err,
|
|
416
792
|
}),
|
|
@@ -418,37 +794,122 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
418
794
|
const args = (raw ?? {});
|
|
419
795
|
const result = flatSearchResult(await catalog.search({
|
|
420
796
|
...args,
|
|
797
|
+
includeSchemas: args.includeSchemas ?? "json",
|
|
421
798
|
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
422
799
|
}));
|
|
423
800
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
801
|
+
for (const tool of result.tools)
|
|
802
|
+
catalogAddressDependency(tool.address);
|
|
803
|
+
for (const error of result.catalogErrors)
|
|
804
|
+
catalogDependency(error.connector);
|
|
805
|
+
for (const connector of result.absence?.configuredConnectors ?? [])
|
|
806
|
+
catalogDependency(connector);
|
|
807
|
+
for (const connector of result.queryAnalysis?.configuredConnectors ?? [])
|
|
808
|
+
catalogDependency(connector);
|
|
809
|
+
if (result.queryAnalysis?.connectorScope)
|
|
810
|
+
catalogDependency(result.queryAnalysis.connectorScope);
|
|
424
811
|
return result;
|
|
425
812
|
}),
|
|
426
813
|
describe: (raw) => discovery("describe", async () => {
|
|
427
814
|
const args = (raw ?? {});
|
|
428
|
-
const result = { tools: await catalog.describe(args) };
|
|
815
|
+
const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
|
|
429
816
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
817
|
+
for (const tool of result.tools) {
|
|
818
|
+
if (!tool.error)
|
|
819
|
+
catalogAddressDependency(tool.address);
|
|
820
|
+
else {
|
|
821
|
+
const resolved = registry.resolveAddress(tool.address);
|
|
822
|
+
if (resolved)
|
|
823
|
+
catalogDependency(resolved.connector.id);
|
|
824
|
+
for (const connector of tool.errorDetails?.configuredConnectors ?? [])
|
|
825
|
+
catalogDependency(connector);
|
|
826
|
+
for (const suggestion of tool.errorDetails?.suggestions ?? [])
|
|
827
|
+
catalogAddressDependency(suggestion);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
430
830
|
return result;
|
|
431
831
|
}),
|
|
432
832
|
};
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
const framed = (err) => Effect.suspend(() => {
|
|
833
|
+
// Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
|
|
834
|
+
// Anything that is not an InvocationFailure crosses unchanged.
|
|
835
|
+
const typedFailure = (err) => Effect.suspend(() => {
|
|
437
836
|
if (!(err instanceof InvocationFailure))
|
|
438
837
|
return Effect.fail(err);
|
|
439
|
-
const failure = boundedGuestFailure(err);
|
|
440
|
-
|
|
441
|
-
limits.onInvocationFailure?.(failure);
|
|
442
|
-
return Effect.fail(frame);
|
|
838
|
+
const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
|
|
839
|
+
return Effect.fail(failure);
|
|
443
840
|
});
|
|
444
841
|
return {
|
|
445
842
|
name: "connecta",
|
|
446
843
|
// Trusted host code the program runs after and cannot undo.
|
|
447
|
-
prelude:
|
|
844
|
+
prelude: guestPrelude(),
|
|
448
845
|
fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
|
|
449
846
|
name,
|
|
450
847
|
(...args) => {
|
|
451
|
-
|
|
848
|
+
if (budgetFailure)
|
|
849
|
+
return stopped;
|
|
850
|
+
// Invalid emits still allocate guest promises and typed rejections.
|
|
851
|
+
// Bound those attempts without consuming the invocation budget or discarding identities.
|
|
852
|
+
if (name === "emit" && ++emitAttempts > maxEmitAttempts) {
|
|
853
|
+
budgetFailure = guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`);
|
|
854
|
+
programWrites?.close();
|
|
855
|
+
limits.dispatchController?.abort();
|
|
856
|
+
limits.onHostCallBudgetExceeded?.(budgetFailure);
|
|
857
|
+
return stopped;
|
|
858
|
+
}
|
|
859
|
+
// L4/M7: spend on entry, before even invalid arguments are checked;
|
|
860
|
+
// emit has a separate budget and never spends this one.
|
|
861
|
+
// Transparent transfer pages consume the already-admitted call, not guest host calls.
|
|
862
|
+
const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
|
|
863
|
+
if (counted && ++hostCalls.attempted > maxHostCalls) {
|
|
864
|
+
hostCalls.failed++;
|
|
865
|
+
budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
|
|
866
|
+
programWrites?.close();
|
|
867
|
+
limits.dispatchController?.abort();
|
|
868
|
+
limits.onHostCallBudgetExceeded?.(budgetFailure);
|
|
869
|
+
return stopped;
|
|
870
|
+
}
|
|
871
|
+
if (counted)
|
|
872
|
+
hostCalls.admitted++;
|
|
873
|
+
const started = Date.now();
|
|
874
|
+
const utility = name === "result" || name === "skill";
|
|
875
|
+
const invoke = Effect.suspend(() => operation(...args));
|
|
876
|
+
const guarded = utility
|
|
877
|
+
? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal)), {
|
|
878
|
+
timeoutMs: hostCallTimeoutMs,
|
|
879
|
+
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
880
|
+
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
881
|
+
})
|
|
882
|
+
: invoke;
|
|
883
|
+
const settled = runEdge(guarded.pipe(Effect.catch((error) => {
|
|
884
|
+
if (utility && hostAccessSignal?.aborted)
|
|
885
|
+
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
886
|
+
if (utility && error instanceof InvocationFailure && error.code === "timeout") {
|
|
887
|
+
const elapsedMs = Date.now() - started;
|
|
888
|
+
const message = `Operation "connecta.${name}" timed out during ${name === "result" ? "result storage" : "skill lookup"} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
|
|
889
|
+
return typedFailure(new InvocationFailure({
|
|
890
|
+
code: "timeout",
|
|
891
|
+
message,
|
|
892
|
+
retryable: true,
|
|
893
|
+
details: {
|
|
894
|
+
operation: `connecta.${name}`,
|
|
895
|
+
stage: name === "result" ? "result storage" : "skill lookup",
|
|
896
|
+
elapsedMs,
|
|
897
|
+
deadlineMs: hostCallTimeoutMs,
|
|
898
|
+
},
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
901
|
+
return typedFailure(error);
|
|
902
|
+
}))).then((value) => {
|
|
903
|
+
if (counted)
|
|
904
|
+
hostCalls.succeeded++;
|
|
905
|
+
return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
|
|
906
|
+
}, (err) => {
|
|
907
|
+
if (counted)
|
|
908
|
+
hostCalls.failed++;
|
|
909
|
+
if (budgetFailure)
|
|
910
|
+
return stopped;
|
|
911
|
+
throw redactAgentOutput(sentSecrets, err);
|
|
912
|
+
});
|
|
452
913
|
// The executor owns this promise, and a program may abandon a call
|
|
453
914
|
// that the run's end then cancels before anyone has awaited it.
|
|
454
915
|
// The rejection is still there for whoever does; it is just not an
|
|
@@ -476,11 +937,12 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
476
937
|
* ceiling as an untyped executor failure naming the key that sets it. Either
|
|
477
938
|
* way the handler returns and releases the lease, and the abandoned call
|
|
478
939
|
* keeps whatever it was doing: the QuickJS lease release recycles its child,
|
|
479
|
-
*
|
|
480
|
-
* after its request gave up goes to `late`. The result contract is untouched
|
|
940
|
+
* the Worker adapter disposes its handles without waiting for the guest. A
|
|
941
|
+
* lease granted after its request gave up goes to `late`. The result contract is untouched
|
|
481
942
|
* — this only decides when to stop waiting for one.
|
|
482
943
|
*/
|
|
483
944
|
function awaitExecutor(start, signal, options = {}) {
|
|
945
|
+
const started = Date.now();
|
|
484
946
|
const contenders = [
|
|
485
947
|
Effect.suspend(() => {
|
|
486
948
|
let pending;
|
|
@@ -501,12 +963,21 @@ function awaitExecutor(start, signal, options = {}) {
|
|
|
501
963
|
// which the executor's promise chain can still lose to.)
|
|
502
964
|
fromSignal(signal).pipe(Effect.catch(() => Effect.sleep(Duration.millis(1))), Effect.andThen(Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled.")))),
|
|
503
965
|
];
|
|
966
|
+
if (options.terminal)
|
|
967
|
+
contenders.push(options.terminal);
|
|
504
968
|
const { watchdog } = options;
|
|
505
969
|
if (watchdog) {
|
|
506
970
|
contenders.push(Effect.sleep(Duration.millis(watchdog.ms)).pipe(Effect.andThen(Effect.suspend(() => {
|
|
507
|
-
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
|
|
508
|
-
|
|
509
|
-
|
|
971
|
+
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
|
|
972
|
+
watchdogMs: watchdog.ms,
|
|
973
|
+
});
|
|
974
|
+
const elapsedMs = Date.now() - started;
|
|
975
|
+
return Effect.fail(new InvocationFailure({
|
|
976
|
+
code: "timeout",
|
|
977
|
+
message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${watchdog.ms}ms); sandbox unresponsive.`,
|
|
978
|
+
retryable: false,
|
|
979
|
+
details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs: watchdog.ms },
|
|
980
|
+
}));
|
|
510
981
|
}))));
|
|
511
982
|
}
|
|
512
983
|
return Effect.raceAllFirst(contenders);
|
|
@@ -540,44 +1011,40 @@ function runSignal(caller) {
|
|
|
540
1011
|
* which workerd refuses for any I/O. Awaiting the promise resumes it here.
|
|
541
1012
|
*/
|
|
542
1013
|
function leased(executor, signal) {
|
|
543
|
-
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal }), signal, {
|
|
1014
|
+
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
|
|
544
1015
|
late: (lease) => lease.release(),
|
|
545
1016
|
}), (lease) => Effect.sync(() => lease.release()));
|
|
546
1017
|
}
|
|
547
|
-
/**
|
|
548
|
-
const RUN_CLAIM_SLACK_MS = 10_000;
|
|
549
|
-
/**
|
|
550
|
-
* How long one run may take before whoever claimed it may give up on it: the
|
|
551
|
-
* watchdog, one more host-call deadline for an exempt write the play drains,
|
|
552
|
-
* and queue slack. Artifact refresh leases its claim for this long.
|
|
553
|
-
*/
|
|
554
|
-
export function runClaimMs(config) {
|
|
555
|
-
return resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS) +
|
|
556
|
-
resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS) +
|
|
557
|
-
RUN_CLAIM_SLACK_MS;
|
|
558
|
-
}
|
|
559
|
-
/** The execute_code handler. Exported for direct testing and artifact refresh. */
|
|
1018
|
+
/** The execute_code handler. Exported for direct testing. */
|
|
560
1019
|
export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
|
|
561
1020
|
const watchdog = {
|
|
562
1021
|
ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
|
|
563
1022
|
logger,
|
|
564
1023
|
};
|
|
565
1024
|
const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
|
|
566
|
-
const play = (program, diagnosticsRequested, callerSignal) => Effect.suspend(() => {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
: undefined;
|
|
1025
|
+
const play = (program, diagnosticsRequested, callerSignal, requestScope) => Effect.suspend(() => {
|
|
1026
|
+
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
1027
|
+
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
570
1028
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
571
|
-
const
|
|
572
|
-
const
|
|
1029
|
+
const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
1030
|
+
const programWrites = new ProgramWrites();
|
|
1031
|
+
const resultDependencies = [];
|
|
1032
|
+
const dispatchController = new AbortController();
|
|
1033
|
+
const terminal = Deferred.makeUnsafe();
|
|
1034
|
+
let budgetFailure;
|
|
1035
|
+
let executorLogs;
|
|
573
1036
|
// The run's scope holds its signal and its lease. However the run
|
|
574
1037
|
// ends — a result, a thrown executor, the watchdog, cancellation —
|
|
575
1038
|
// closing it releases the lease and then aborts the signal, so
|
|
576
1039
|
// nothing the run started outlives the request.
|
|
577
1040
|
const run = Effect.gen(function* () {
|
|
1041
|
+
// Per-call signals cancel individual fetches, but do not close the
|
|
1042
|
+
// shared transport. Close its scope after the run's signal ends, on
|
|
1043
|
+
// every exit, using the existing bounded cleanup and deferred tail.
|
|
1044
|
+
yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
|
|
578
1045
|
const signal = yield* runSignal(callerSignal);
|
|
579
1046
|
// Admission comes before provider construction: queued calls retain
|
|
580
|
-
// no catalogs
|
|
1047
|
+
// no catalogs or provider closures.
|
|
581
1048
|
let lease;
|
|
582
1049
|
if (isAdmittingExecutor(executor)) {
|
|
583
1050
|
lease = yield* timed((elapsed) => {
|
|
@@ -594,11 +1061,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
594
1061
|
if (diagnostics)
|
|
595
1062
|
diagnostics.setupMs = elapsed;
|
|
596
1063
|
}, Effect.sync(() => sandboxProvider(registry, baseUrl, activity, {
|
|
1064
|
+
sentSecrets,
|
|
597
1065
|
signal,
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1066
|
+
hostCalls,
|
|
1067
|
+
onHostCallBudgetExceeded: (failure) => {
|
|
1068
|
+
budgetFailure = failure;
|
|
1069
|
+
Deferred.doneUnsafe(terminal, Effect.fail(failure));
|
|
602
1070
|
},
|
|
603
1071
|
emitCollector: emitted,
|
|
604
1072
|
...(diagnostics ? { diagnostics } : {}),
|
|
@@ -606,62 +1074,111 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
606
1074
|
probeTimeoutMs: config.probeTimeoutMs,
|
|
607
1075
|
maxHostCalls: config.maxHostCalls,
|
|
608
1076
|
hostCallTimeoutMs,
|
|
1077
|
+
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
1078
|
+
programDeadlineAt: Date.now() + watchdog.ms,
|
|
609
1079
|
defer: config.defer,
|
|
610
|
-
|
|
1080
|
+
trust: config.trust,
|
|
611
1081
|
maxWrites: config.maxWrites,
|
|
612
|
-
|
|
613
|
-
|
|
1082
|
+
programWrites,
|
|
1083
|
+
resultDependencies,
|
|
1084
|
+
dispatchController,
|
|
1085
|
+
}, requestScope)));
|
|
614
1086
|
if (signal.aborted) {
|
|
615
1087
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
616
1088
|
}
|
|
617
1089
|
const admitted = lease;
|
|
618
|
-
//
|
|
1090
|
+
// A trusted-pool write may still be on the wire when the program settles:
|
|
619
1091
|
// close, so a write gated after this is not sent, then let the ones
|
|
620
1092
|
// already dispatched finish before the scope aborts them — an
|
|
621
1093
|
// abandoned write would be one whose outcome nobody knows.
|
|
622
1094
|
return yield* timed((elapsed) => {
|
|
623
1095
|
if (diagnostics)
|
|
624
1096
|
diagnostics.executorWallMs = elapsed;
|
|
625
|
-
}, awaitExecutor(() => admitted
|
|
626
|
-
? admitted.execute(program, [provider])
|
|
627
|
-
: executor.execute(program, [provider])
|
|
628
|
-
|
|
629
|
-
return
|
|
1097
|
+
}, awaitExecutor(() => (admitted
|
|
1098
|
+
? admitted.execute(wrapGuestProgram(program), [provider])
|
|
1099
|
+
: executor.execute(wrapGuestProgram(program), [provider])).then((outcome) => {
|
|
1100
|
+
executorLogs = sentSecrets.redact(outcome?.logs, "prose");
|
|
1101
|
+
return sentSecrets.redact(outcome);
|
|
1102
|
+
}, (err) => {
|
|
1103
|
+
if (err !== null && typeof err === "object" && "logs" in err) {
|
|
1104
|
+
executorLogs = sentSecrets.redact(err.logs, "prose");
|
|
1105
|
+
}
|
|
1106
|
+
throw sentSecrets.redact(err);
|
|
1107
|
+
}), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
|
|
1108
|
+
programWrites.close();
|
|
1109
|
+
dispatchController.abort();
|
|
1110
|
+
return programWrites.drain();
|
|
630
1111
|
}))));
|
|
631
1112
|
});
|
|
632
|
-
const reported = {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1113
|
+
const reported = {
|
|
1114
|
+
emitted,
|
|
1115
|
+
diagnostics,
|
|
1116
|
+
program,
|
|
1117
|
+
guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, config.trust, logger, maxChars, programWrites.hasSucceeded),
|
|
1118
|
+
};
|
|
1119
|
+
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
1120
|
+
// Releasing a QuickJS lease ends its child and rejects execute() with
|
|
1121
|
+
// the retained log prefix. Give that report one timer turn, just as
|
|
1122
|
+
// cancellation does in awaitExecutor; never wait on a Worker isolate.
|
|
1123
|
+
budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
|
|
1124
|
+
return Effect.map(Effect.flatMap(exited, (exit) => {
|
|
1125
|
+
// A synchronous fire-and-forget burst can also settle its executor in
|
|
1126
|
+
// this turn. The host's terminal refusal always wins over that value.
|
|
1127
|
+
if (budgetFailure instanceof HostCallBudgetExceeded) {
|
|
1128
|
+
const failed = failureResponse(budgetFailure.details.message, {
|
|
1129
|
+
code: budgetFailure.details,
|
|
1130
|
+
logs: executeLogs(executorLogs),
|
|
1131
|
+
emitted,
|
|
1132
|
+
diagnostics,
|
|
642
1133
|
});
|
|
1134
|
+
return Effect.succeed(failed);
|
|
643
1135
|
}
|
|
644
|
-
|
|
645
|
-
|
|
1136
|
+
if (Exit.isFailure(exit)) {
|
|
1137
|
+
return Effect.succeed(failedRun(redactAgentOutput(sentSecrets, Cause.squash(exit.cause)), logger, reported));
|
|
1138
|
+
}
|
|
1139
|
+
return Effect.promise(() => finishedRun(redactAgentOutput(sentSecrets, exit.value), reported));
|
|
1140
|
+
}), (unfinished) => {
|
|
1141
|
+
// Every run response passes through write accounting.
|
|
1142
|
+
const response = programWrites.finish(unfinished);
|
|
1143
|
+
// Calls abandoned by the guest are failed when the run cancels them.
|
|
1144
|
+
const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
|
|
1145
|
+
response.structuredContent = { ...response.structuredContent, hostCalls: counts };
|
|
1146
|
+
if (response.content[0]?.type === "text")
|
|
1147
|
+
response.content[0].text = JSON.stringify(response.structuredContent);
|
|
1148
|
+
return response;
|
|
1149
|
+
}).pipe(Effect.map((result) => redactAgentOutput(sentSecrets, result)));
|
|
646
1150
|
});
|
|
647
|
-
return (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
code: { code: "invalid_args", message, retryable: false }
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1151
|
+
return agentOutputOperations((requestScope) => ({
|
|
1152
|
+
execute: ({ code, diagnostics }, options = {}) => {
|
|
1153
|
+
// A code-unit count above the cap is already too large in UTF-8. Check
|
|
1154
|
+
// that first so a huge direct-call string is never encoded in full.
|
|
1155
|
+
if (code.length > EXECUTE_MAX_CODE_BYTES ||
|
|
1156
|
+
new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
|
|
1157
|
+
const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
|
|
1158
|
+
const response = failureResponse(message, { code: { code: "invalid_args", message, retryable: false } });
|
|
1159
|
+
response.structuredContent = {
|
|
1160
|
+
...response.structuredContent,
|
|
1161
|
+
hostCalls: { attempted: 0, admitted: 0, succeeded: 0, failed: 0 },
|
|
1162
|
+
};
|
|
1163
|
+
response.content[0].text = JSON.stringify(response.structuredContent);
|
|
1164
|
+
return Promise.resolve(response);
|
|
1165
|
+
}
|
|
1166
|
+
return runEdge(Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal, requestScope)));
|
|
1167
|
+
},
|
|
1168
|
+
}), config.requestScope).execute;
|
|
659
1169
|
}
|
|
660
1170
|
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
|
661
|
-
function failedRun(err, logger, { emitted, diagnostics }) {
|
|
662
|
-
const logs = err !== null && typeof err === "object" && "logs" in err
|
|
663
|
-
|
|
664
|
-
:
|
|
1171
|
+
function failedRun(err, logger, { emitted, diagnostics, program }) {
|
|
1172
|
+
const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
|
|
1173
|
+
if (err instanceof InvocationFailure)
|
|
1174
|
+
return failureResponse(err.details.message, { logs, emitted, diagnostics, code: err.details });
|
|
1175
|
+
if (err instanceof SyntaxError)
|
|
1176
|
+
return failureResponse(err.message, {
|
|
1177
|
+
logs,
|
|
1178
|
+
emitted,
|
|
1179
|
+
diagnostics,
|
|
1180
|
+
code: programError({ name: "SyntaxError", message: err.message }, program),
|
|
1181
|
+
});
|
|
665
1182
|
if (err instanceof ExecutorAdmissionError) {
|
|
666
1183
|
if (err.code === "executor_overloaded") {
|
|
667
1184
|
logger.warn("[connecta] execute_code admission rejected", {
|
|
@@ -677,9 +1194,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
|
|
|
677
1194
|
code: err.code,
|
|
678
1195
|
message: err.message,
|
|
679
1196
|
retryable: err.retryable,
|
|
680
|
-
...(err.retryAfterMs !== undefined
|
|
681
|
-
? { retryAfterMs: err.retryAfterMs }
|
|
682
|
-
: {}),
|
|
1197
|
+
...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
|
|
683
1198
|
},
|
|
684
1199
|
});
|
|
685
1200
|
}
|
|
@@ -691,7 +1206,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
|
|
|
691
1206
|
});
|
|
692
1207
|
}
|
|
693
1208
|
/** The response for an ExecuteResult: the program's error or its value. */
|
|
694
|
-
function finishedRun(outcome, { emitted, diagnostics,
|
|
1209
|
+
async function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
|
|
695
1210
|
if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
|
|
696
1211
|
return failureResponse("Executor failed: expected an ExecuteResult object.", {
|
|
697
1212
|
emitted,
|
|
@@ -709,52 +1224,32 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
|
|
|
709
1224
|
});
|
|
710
1225
|
}
|
|
711
1226
|
if (outcome.error !== undefined) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
//
|
|
718
|
-
// An error the program let through unchanged matches exactly, and an
|
|
719
|
-
// exact match always wins: a program that wrapped one failure's message
|
|
720
|
-
// around another's must not have the wrong type attached. Containment is
|
|
721
|
-
// the fallback, so a wrapped message still reports its underlying type
|
|
722
|
-
// rather than losing it to prose.
|
|
723
|
-
let invocationFailure;
|
|
724
|
-
for (const match of [
|
|
725
|
-
(candidate) => outcome.error !== "" &&
|
|
726
|
-
[candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
|
|
727
|
-
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) =>
|
|
728
|
-
// E6: empty or tiny prose cannot identify a wrapped failure.
|
|
729
|
-
message !== undefined &&
|
|
730
|
-
message.length >= 8 &&
|
|
731
|
-
outcome.error?.includes(message) === true),
|
|
732
|
-
]) {
|
|
733
|
-
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
734
|
-
const candidate = invocationFailures[i];
|
|
735
|
-
if (candidate && match(candidate)) {
|
|
736
|
-
invocationFailure = candidate;
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (invocationFailure)
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
if (invocationFailure) {
|
|
744
|
-
// E1/X11: return the same bounded details the guest received.
|
|
745
|
-
return failureResponse(invocationFailure.details.message, {
|
|
1227
|
+
const failure = outcome.failure?.call;
|
|
1228
|
+
if (outcome.failure?.timeout) {
|
|
1229
|
+
const { elapsedMs, deadlineMs } = outcome.failure.timeout;
|
|
1230
|
+
const message = `Operation "execute_code" timed out during sandbox after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`;
|
|
1231
|
+
return failureResponse(message, {
|
|
746
1232
|
logs,
|
|
747
1233
|
emitted,
|
|
748
1234
|
diagnostics,
|
|
749
|
-
code:
|
|
1235
|
+
code: {
|
|
1236
|
+
code: "timeout",
|
|
1237
|
+
message,
|
|
1238
|
+
retryable: false,
|
|
1239
|
+
details: { operation: "execute_code", stage: "sandbox", elapsedMs, deadlineMs },
|
|
1240
|
+
},
|
|
750
1241
|
});
|
|
751
1242
|
}
|
|
752
|
-
|
|
753
|
-
return failureResponse(message, {
|
|
1243
|
+
return failureResponse(failure?.message ?? outcome.error, {
|
|
754
1244
|
logs,
|
|
755
1245
|
emitted,
|
|
756
1246
|
diagnostics,
|
|
757
|
-
code:
|
|
1247
|
+
code: failure ??
|
|
1248
|
+
programError({
|
|
1249
|
+
name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
|
|
1250
|
+
message: outcome.error || "Execution failed without an error message.",
|
|
1251
|
+
...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
|
|
1252
|
+
}, program),
|
|
758
1253
|
});
|
|
759
1254
|
}
|
|
760
1255
|
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
@@ -762,7 +1257,7 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
|
|
|
762
1257
|
// error path so captured logs survive instead of a raw SDK 500.
|
|
763
1258
|
let result;
|
|
764
1259
|
try {
|
|
765
|
-
result =
|
|
1260
|
+
result = await guardResult(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
|
|
766
1261
|
}
|
|
767
1262
|
catch (err) {
|
|
768
1263
|
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
@@ -794,33 +1289,20 @@ function executeLogs(value) {
|
|
|
794
1289
|
}
|
|
795
1290
|
function failureResponse(message, options) {
|
|
796
1291
|
const { logs, emitted, diagnostics, code } = options;
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
});
|
|
806
|
-
result.isError = true;
|
|
807
|
-
return result;
|
|
808
|
-
}
|
|
809
|
-
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${emitted ? discardedEmitsText(emitted) : ""}`);
|
|
1292
|
+
const result = jsonResult({
|
|
1293
|
+
error: typeof code === "string" ? { code, message, retryable: false } : code,
|
|
1294
|
+
...(logs ? { logs } : {}),
|
|
1295
|
+
...(emitted ? discardedEmits(emitted) : {}),
|
|
1296
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
1297
|
+
});
|
|
1298
|
+
result.isError = true;
|
|
1299
|
+
return result;
|
|
810
1300
|
}
|
|
811
1301
|
/**
|
|
812
1302
|
* A failed program delivers no blocks and reports how many were discarded.
|
|
813
1303
|
*/
|
|
814
1304
|
function discardedEmits(emitted) {
|
|
815
|
-
return emitted.blocks.length > 0
|
|
816
|
-
? { emittedDiscarded: emitted.blocks.length }
|
|
817
|
-
: {};
|
|
818
|
-
}
|
|
819
|
-
/** The same visibility for the plain-text error paths. */
|
|
820
|
-
function discardedEmitsText(emitted) {
|
|
821
|
-
return emitted.blocks.length > 0
|
|
822
|
-
? `\n\nemittedDiscarded: ${emitted.blocks.length}`
|
|
823
|
-
: "";
|
|
1305
|
+
return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
|
|
824
1306
|
}
|
|
825
1307
|
function connectorInventory(connectors) {
|
|
826
1308
|
const prefix = "Connectors: ";
|
|
@@ -829,14 +1311,10 @@ function connectorInventory(connectors) {
|
|
|
829
1311
|
const entries = connectors.map((connector) => {
|
|
830
1312
|
const address = connector.id;
|
|
831
1313
|
const title = connector.title?.replace(/\s+/g, " ").trim();
|
|
832
|
-
const label = title && title !== connector.id
|
|
833
|
-
? `${address}: ${boundedEchoText(title, 45)}`
|
|
834
|
-
: address;
|
|
1314
|
+
const label = title && title !== connector.id ? `${address}: ${boundedEchoText(title, 45)}` : address;
|
|
835
1315
|
if (!connectorGuide(connector))
|
|
836
1316
|
return label;
|
|
837
|
-
const requirement = connectorGuideRequired(connector)
|
|
838
|
-
? "required guide"
|
|
839
|
-
: "guide";
|
|
1317
|
+
const requirement = connectorGuideRequired(connector) ? "required guide" : "guide";
|
|
840
1318
|
return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
|
|
841
1319
|
});
|
|
842
1320
|
const shown = [];
|
|
@@ -858,29 +1336,26 @@ function connectorInventory(connectors) {
|
|
|
858
1336
|
return `${prefix}${shown.join(", ")}.`;
|
|
859
1337
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
860
1338
|
}
|
|
861
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites) => `
|
|
862
|
-
|
|
1339
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, trust) => `One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. ${trust === "trusted" ? "Trusted pool: programs may read and write; the host approves the program as a write." : "Read-only pool: programs read; writes use call_destructive_tool."} Limits: ${hostLimits.maxHostCalls} host calls, ${maxWrites} writes, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
863
1340
|
${connectorInventory(connectors)}
|
|
864
1341
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1342
|
+
Use async () => { ... }, no arguments. No portable ambient capabilities. API:
|
|
1343
|
+
\`\`\`ts
|
|
1344
|
+
declare const connecta: {
|
|
1345
|
+
${GUEST_API_DECLARATION}
|
|
1346
|
+
};
|
|
1347
|
+
\`\`\`
|
|
1348
|
+
Search: {tools}, catalogErrors. JSON schemas default; compact text. Calls: {data, format:"json"|"text"}; >1 MiB: paged handle. Oversized returns: resultId/nextAction. connecta.result pages; UTF-8 byte offsets. allSettled keeps typed errors; host budget_exceeded ends the run.
|
|
871
1349
|
|
|
872
|
-
|
|
873
|
-
// Module scope, like the other
|
|
1350
|
+
Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
|
|
1351
|
+
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
874
1352
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
875
1353
|
// view, so they live in the description, never here.
|
|
876
1354
|
const EXECUTE_INPUT = advertisedSchema(z.object({
|
|
877
1355
|
code: z
|
|
878
1356
|
.string()
|
|
879
1357
|
.describe("One complete zero-argument JavaScript async arrow: async () => { ... }. Use the provided connecta global to discover, call, and return the reduced answer. At most 65,536 UTF-8 bytes."),
|
|
880
|
-
diagnostics: z
|
|
881
|
-
.boolean()
|
|
882
|
-
.optional()
|
|
883
|
-
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
1358
|
+
diagnostics: z.boolean().optional().describe("Add request-local, payload-free timing and result-size summaries."),
|
|
884
1359
|
}));
|
|
885
1360
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
886
1361
|
export function registerExecuteTool(server, registry, ctx) {
|
|
@@ -899,6 +1374,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
899
1374
|
const connectors = registry.listConnectors();
|
|
900
1375
|
const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
|
|
901
1376
|
const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
|
|
1377
|
+
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
902
1378
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
903
1379
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
904
1380
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
@@ -907,25 +1383,29 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
907
1383
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
908
1384
|
watchdogMs: ctx.watchdogMs,
|
|
909
1385
|
defer: ctx.defer,
|
|
910
|
-
|
|
1386
|
+
trust: ctx.trust,
|
|
911
1387
|
maxWrites,
|
|
1388
|
+
requestScope: ctx.requestScope,
|
|
912
1389
|
});
|
|
913
1390
|
server.registerTool("execute_code", {
|
|
914
|
-
description: executeDescription(emitBudgets,
|
|
1391
|
+
description: executeDescription(emitBudgets, {
|
|
1392
|
+
...hostLimits,
|
|
1393
|
+
hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1394
|
+
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.trust ?? "read-only"),
|
|
915
1395
|
inputSchema: EXECUTE_INPUT,
|
|
916
|
-
|
|
917
|
-
//
|
|
918
|
-
// deployment's decision, which no annotation can make for it. Any
|
|
919
|
-
// other write is refused (E4) and crosses call_destructive_tool, which
|
|
920
|
-
// is annotated destructive. The supported executor constructions deny
|
|
921
|
-
// outbound access, filesystem, and deployment config; X5 documents
|
|
922
|
-
// Dynamic runtime modules separately.
|
|
1396
|
+
outputSchema: EXECUTE_OUTPUT,
|
|
1397
|
+
// The host sees execute_code as a write only on a trusted endpoint.
|
|
923
1398
|
annotations: {
|
|
924
|
-
readOnlyHint:
|
|
925
|
-
destructiveHint:
|
|
1399
|
+
readOnlyHint: ctx.trust !== "trusted",
|
|
1400
|
+
destructiveHint: ctx.trust === "trusted",
|
|
926
1401
|
openWorldHint: true,
|
|
927
1402
|
},
|
|
928
|
-
}, (args, extra) =>
|
|
929
|
-
|
|
930
|
-
|
|
1403
|
+
}, (args, extra) => {
|
|
1404
|
+
if (ctx.client)
|
|
1405
|
+
bindMcpClient(extra, ctx.client, ctx.activity);
|
|
1406
|
+
const run = () => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
|
|
1407
|
+
signal,
|
|
1408
|
+
}));
|
|
1409
|
+
return ctx.authElicitation ? ctx.authElicitation.run("execute_code", args, extra, run) : run();
|
|
1410
|
+
});
|
|
931
1411
|
}
|