@zackbart/connecta 0.28.1 → 0.29.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 +788 -5
- package/README.md +62 -42
- package/bin/connecta.mjs +110 -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/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 +283 -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 +368 -224
- 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 +631 -0
- package/dist/config.js +503 -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 +84 -35
- 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 +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- 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 +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -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 +230 -73
- 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 +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- 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 +117 -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-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- 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/{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/{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 +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -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 +356 -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 +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -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 +320 -54
- 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 +137 -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 +434 -0
- package/documentation/architecture.md +664 -231
- package/documentation/auth.md +936 -546
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -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 +120 -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 +133 -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,28 @@
|
|
|
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 { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
|
|
7
|
-
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor
|
|
8
|
-
import { boundedEchoText, msg } from "./errors.js";
|
|
9
|
-
import { DEFAULT_MAX_WRITES,
|
|
10
|
-
import { InvocationFailure, InvocationService, timed
|
|
8
|
+
import { createMetaTools, jsonResult } from "./meta-tools.js";
|
|
9
|
+
import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText, } from "./executor-result.js";
|
|
10
|
+
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
|
|
11
|
+
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
12
|
+
import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
|
|
13
|
+
import { InvocationFailure, InvocationService, timed } from "./invocation.js";
|
|
14
|
+
import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
|
|
15
|
+
import { EXECUTE_OUTPUT } from "./meta-output.js";
|
|
11
16
|
import { normalizeProgramSource } from "./program-source.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
17
|
+
import { readResource } from "./resource-read.js";
|
|
18
|
+
import { agentOutputOperations, redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
|
|
19
|
+
import { normalizeTimeoutMs, underAnySignal } from "./timeout.js";
|
|
20
|
+
import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
21
|
+
import { closeScope } from "./runtime/connector-scope.js";
|
|
22
|
+
import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
|
|
16
23
|
/** 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 =
|
|
24
|
+
const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
|
|
25
|
+
const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
19
26
|
/** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
|
|
20
27
|
const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
21
28
|
/**
|
|
@@ -24,7 +31,7 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
|
24
31
|
* always reports its own timeout first and this fires only for one that
|
|
25
32
|
* never settles at all.
|
|
26
33
|
*/
|
|
27
|
-
const EXECUTE_WATCHDOG_MS =
|
|
34
|
+
const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
|
|
28
35
|
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
29
36
|
export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
30
37
|
/**
|
|
@@ -34,8 +41,9 @@ export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
|
34
41
|
* two or three real screenshots after base64's 4/3 inflation, well short of a
|
|
35
42
|
* file-hosting ambition (design record M5).
|
|
36
43
|
*/
|
|
37
|
-
export const EXECUTE_MAX_EMITTED_BYTES =
|
|
38
|
-
export const EXECUTE_MAX_EMITTED_BLOCKS =
|
|
44
|
+
export const EXECUTE_MAX_EMITTED_BYTES = CONFIG_DEFAULTS.execute.maxEmittedBytes;
|
|
45
|
+
export const EXECUTE_MAX_EMITTED_BLOCKS = CONFIG_DEFAULTS.execute.maxEmittedBlocks;
|
|
46
|
+
const CALL_SIGNATURE = "Use connecta.call(address, args?, { timeoutMs? }) or connecta.call({ address, args?, timeoutMs? }). address must be a canonical connector.tool string.";
|
|
39
47
|
const diagnosticsEncoder = new TextEncoder();
|
|
40
48
|
class ExecuteDiagnostics {
|
|
41
49
|
started = Date.now();
|
|
@@ -85,6 +93,15 @@ class ExecuteDiagnostics {
|
|
|
85
93
|
stats.catalogMs += outcome.timing.catalogMs;
|
|
86
94
|
stats.connectorMs += outcome.timing.connectorMs;
|
|
87
95
|
}
|
|
96
|
+
recordRead(durationMs, connectorMs, ok, value) {
|
|
97
|
+
const stats = this.stats("read");
|
|
98
|
+
stats.count++;
|
|
99
|
+
stats.failures += ok ? 0 : 1;
|
|
100
|
+
stats.durationMs += durationMs;
|
|
101
|
+
stats.connectorMs += connectorMs;
|
|
102
|
+
if (ok)
|
|
103
|
+
stats.resultBytes += serializedDiagnosticBytes(value);
|
|
104
|
+
}
|
|
88
105
|
finish() {
|
|
89
106
|
const operations = [...this.operations.values()];
|
|
90
107
|
return {
|
|
@@ -130,6 +147,19 @@ function requireEmittedBlock(raw) {
|
|
|
130
147
|
if (extra.length > 0) {
|
|
131
148
|
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
149
|
}
|
|
150
|
+
if (block.type === "image") {
|
|
151
|
+
if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(block.mimeType)) {
|
|
152
|
+
throw guestFailure("invalid_args", "connecta.emit image mimeType must be image/png, image/jpeg, image/gif, or image/webp.");
|
|
153
|
+
}
|
|
154
|
+
const data = block.data;
|
|
155
|
+
// RFC 4648 alphabet, complete quads and canonical padding, including zero
|
|
156
|
+
// pad bits. Decode only the last quad; validation does not copy the image.
|
|
157
|
+
if (!data.length ||
|
|
158
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data) ||
|
|
159
|
+
btoa(atob(data.slice(-4))) !== data.slice(-4)) {
|
|
160
|
+
throw guestFailure("invalid_args", "connecta.emit image data must be nonempty, strict base64 with canonical padding.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
133
163
|
return raw;
|
|
134
164
|
}
|
|
135
165
|
export class EmitCollector {
|
|
@@ -138,6 +168,7 @@ export class EmitCollector {
|
|
|
138
168
|
diagnostics;
|
|
139
169
|
blocks = [];
|
|
140
170
|
bytes = 0;
|
|
171
|
+
textChars = 0;
|
|
141
172
|
constructor(maxBytes, maxBlocks, diagnostics) {
|
|
142
173
|
this.maxBytes = maxBytes;
|
|
143
174
|
this.maxBlocks = maxBlocks;
|
|
@@ -153,6 +184,12 @@ export class EmitCollector {
|
|
|
153
184
|
if (this.bytes + size > this.maxBytes) {
|
|
154
185
|
throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
|
|
155
186
|
}
|
|
187
|
+
const textChars = block.type === "text" ? JSON.stringify(block).length : 0;
|
|
188
|
+
// Reserve space for a reduced result envelope even if text fills the budget.
|
|
189
|
+
if (this.textChars + textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
|
|
190
|
+
throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
|
|
191
|
+
}
|
|
192
|
+
this.textChars += textChars;
|
|
156
193
|
this.blocks.push(block);
|
|
157
194
|
this.bytes += size;
|
|
158
195
|
this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
|
|
@@ -160,16 +197,12 @@ export class EmitCollector {
|
|
|
160
197
|
}
|
|
161
198
|
/** A positive whole-number budget, or the default when the value is unusable. */
|
|
162
199
|
function resolveBudget(value, fallback) {
|
|
163
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 1
|
|
164
|
-
? Math.trunc(value)
|
|
165
|
-
: fallback;
|
|
200
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
|
|
166
201
|
}
|
|
167
202
|
function serializedDiagnosticBytes(value) {
|
|
168
203
|
try {
|
|
169
204
|
const text = JSON.stringify(value);
|
|
170
|
-
return text === undefined
|
|
171
|
-
? 0
|
|
172
|
-
: diagnosticsEncoder.encode(text).byteLength;
|
|
205
|
+
return text === undefined ? 0 : diagnosticsEncoder.encode(text).byteLength;
|
|
173
206
|
}
|
|
174
207
|
catch {
|
|
175
208
|
// The executor's normal result guard owns the error. Diagnostics must
|
|
@@ -180,16 +213,8 @@ function serializedDiagnosticBytes(value) {
|
|
|
180
213
|
function guestFailure(code, message, retryable = false) {
|
|
181
214
|
return new InvocationFailure({ code, message, retryable });
|
|
182
215
|
}
|
|
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
216
|
/**
|
|
192
|
-
* E1/X11: bound
|
|
217
|
+
* E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
|
|
193
218
|
* Keep optional recovery whole: clipping an address or echoed args could turn
|
|
194
219
|
* recovery into a different call. Oversized metadata is omitted instead.
|
|
195
220
|
*/
|
|
@@ -216,67 +241,17 @@ function boundedGuestFailure(failure) {
|
|
|
216
241
|
code: boundText(failure.details.code, 128),
|
|
217
242
|
message: boundText(failure.details.message, 2_000),
|
|
218
243
|
};
|
|
219
|
-
//
|
|
220
|
-
// 4,000-character error bound, with room for bridge-added context.
|
|
244
|
+
// Keep typed metadata below the executor bridge bound.
|
|
221
245
|
if (JSON.stringify(details).length > 3_700) {
|
|
222
246
|
details = {
|
|
223
247
|
code: details.code,
|
|
224
248
|
message: details.message,
|
|
225
249
|
retryable: details.retryable,
|
|
226
|
-
...(details.retryAfterMs !== undefined
|
|
227
|
-
? { retryAfterMs: details.retryAfterMs }
|
|
228
|
-
: {}),
|
|
250
|
+
...(details.retryAfterMs !== undefined ? { retryAfterMs: details.retryAfterMs } : {}),
|
|
229
251
|
};
|
|
230
252
|
}
|
|
231
253
|
return new InvocationFailure(details);
|
|
232
254
|
}
|
|
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
255
|
/**
|
|
281
256
|
* Expose one host provider and typed guest errors. Catalogs load only when
|
|
282
257
|
* the program calls a tool or asks search/describe.
|
|
@@ -284,58 +259,78 @@ function guestErrorPrelude(failureSecret) {
|
|
|
284
259
|
export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
|
|
285
260
|
return [sandboxProvider(registry, baseUrl, activity, limits)];
|
|
286
261
|
}
|
|
262
|
+
/** Numeric accounting only; no addresses, arguments, or results. */
|
|
263
|
+
class HostCallBudgetExceeded extends InvocationFailure {
|
|
264
|
+
hostCalls;
|
|
265
|
+
constructor(hostCalls, maxHostCalls) {
|
|
266
|
+
super({
|
|
267
|
+
code: "budget_exceeded",
|
|
268
|
+
message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
|
|
269
|
+
retryable: false,
|
|
270
|
+
});
|
|
271
|
+
this.hostCalls = hostCalls;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
287
274
|
/**
|
|
288
275
|
* The `connecta` provider for one execution.
|
|
289
276
|
*
|
|
290
277
|
* Building it is synchronous and loads nothing. Each function is a Promise
|
|
291
278
|
* 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
|
-
*
|
|
279
|
+
* the budget, do the operation, and retain a typed failure for the
|
|
280
|
+
* executor bridge to rebuild inside the guest. Nothing is shared between those fibers
|
|
281
|
+
* but the budget counters and this request's catalog. Budget exhaustion ends
|
|
282
|
+
* the whole run, including calls the program never awaits.
|
|
296
283
|
*/
|
|
297
|
-
function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
284
|
+
function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {}) {
|
|
285
|
+
const sentSecrets = sentSecretsForRequest(requestScope, limits.sentSecrets);
|
|
298
286
|
// All host calls made by one execute_code invocation share a downstream
|
|
299
287
|
// connection, while a later invocation receives a fresh request scope.
|
|
300
|
-
const
|
|
288
|
+
const hostAccessSignal = limits.dispatchController
|
|
289
|
+
? AbortSignal.any([limits.dispatchController.signal, ...(limits.signal ? [limits.signal] : [])])
|
|
290
|
+
: limits.signal;
|
|
301
291
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
302
292
|
requestScope,
|
|
293
|
+
requestSignal: hostAccessSignal,
|
|
303
294
|
// A program that just missed an address cannot call search_tools.
|
|
304
295
|
searchRoute: "connecta.search",
|
|
305
296
|
concurrency: limits.discoveryConcurrency,
|
|
306
297
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
307
298
|
defer: limits.defer,
|
|
308
|
-
approval: limits.approval,
|
|
309
299
|
});
|
|
310
300
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
311
301
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
312
302
|
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
|
|
303
|
+
const { signal, diagnostics, programWrites } = limits;
|
|
304
|
+
const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
305
|
+
let budgetFailure;
|
|
306
|
+
let emitAttempts = 0;
|
|
307
|
+
const maxEmitAttempts = Math.max(EXECUTE_MAX_EMITTED_BLOCKS, limits.emitCollector?.maxBlocks ?? EXECUTE_MAX_EMITTED_BLOCKS) * 4;
|
|
308
|
+
// A rejected bridge promise is catchable in both sandboxes. End the host
|
|
309
|
+
// run instead and leave that bridge pending: an awaiting guest cannot catch
|
|
310
|
+
// the refusal and loop. Later calls, including emit, lose host access too.
|
|
311
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
312
|
+
const ended = () => reject(new Error("The host run ended."));
|
|
313
|
+
if (signal?.aborted)
|
|
314
|
+
ended();
|
|
315
|
+
else
|
|
316
|
+
signal?.addEventListener("abort", ended, { once: true });
|
|
317
|
+
});
|
|
318
|
+
// Stay pending until the lease has disposed the sandbox, then settle the
|
|
319
|
+
// abandoned host RPC waits without retaining request resources forever.
|
|
320
|
+
stopped.catch(() => { });
|
|
322
321
|
const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
|
|
323
322
|
let writes = 0;
|
|
324
|
-
/**
|
|
325
|
-
|
|
326
|
-
* the write budget and is tracked until it settles (`ExemptWrites`), so the
|
|
327
|
-
* play can wait for it rather than abort it. Every other write keeps E4's
|
|
328
|
-
* refusal, ahead of validation, so the host's prompt on
|
|
329
|
-
* call_destructive_tool stays the only approval there is.
|
|
330
|
-
*/
|
|
331
|
-
const invocationContext = (sending) => ({
|
|
323
|
+
/** Budget and account for writes admitted by the pool trust decision. */
|
|
324
|
+
const invocationContext = (sending, timeoutMs) => ({
|
|
332
325
|
source: "execute_code",
|
|
333
|
-
|
|
326
|
+
sentSecrets,
|
|
327
|
+
timeoutMs,
|
|
334
328
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
329
|
+
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
335
330
|
unwrapResult: true,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (
|
|
331
|
+
trust: limits.trust,
|
|
332
|
+
beforeWrite: (target) => Effect.sync(() => {
|
|
333
|
+
if (programWrites?.isClosed) {
|
|
339
334
|
return {
|
|
340
335
|
kind: "refuse",
|
|
341
336
|
error: {
|
|
@@ -357,23 +352,53 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
357
352
|
};
|
|
358
353
|
}
|
|
359
354
|
writes++;
|
|
360
|
-
if (
|
|
361
|
-
sending.settle =
|
|
355
|
+
if (programWrites)
|
|
356
|
+
sending.settle = programWrites.begin();
|
|
362
357
|
return { kind: "dispatch" };
|
|
363
358
|
}),
|
|
364
359
|
});
|
|
365
360
|
// A call brings its own cancellation: the invocation pipeline reads the
|
|
366
361
|
// run's signal, refuses a call that starts after it, and records the
|
|
367
362
|
// cancelled attempt in activity like any other outcome.
|
|
368
|
-
const call = (address, args) => Effect.gen(function* () {
|
|
369
|
-
|
|
370
|
-
|
|
363
|
+
const call = (address, args, options) => Effect.gen(function* () {
|
|
364
|
+
let timeout;
|
|
365
|
+
if (address !== null && typeof address === "object" && !Array.isArray(address)) {
|
|
366
|
+
const request = address;
|
|
367
|
+
if (args !== undefined ||
|
|
368
|
+
options !== undefined ||
|
|
369
|
+
Object.keys(request).some((key) => !["address", "args", "timeoutMs"].includes(key))) {
|
|
370
|
+
return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
|
|
371
|
+
}
|
|
372
|
+
address = request.address;
|
|
373
|
+
args = request.args;
|
|
374
|
+
timeout = request.timeoutMs;
|
|
375
|
+
}
|
|
376
|
+
else if (options !== undefined) {
|
|
377
|
+
if (!options ||
|
|
378
|
+
typeof options !== "object" ||
|
|
379
|
+
Array.isArray(options) ||
|
|
380
|
+
Object.keys(options).some((key) => key !== "timeoutMs")) {
|
|
381
|
+
return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
|
|
382
|
+
}
|
|
383
|
+
timeout = options.timeoutMs;
|
|
384
|
+
}
|
|
385
|
+
if (typeof address !== "string" ||
|
|
386
|
+
(timeout !== undefined && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 1))) {
|
|
387
|
+
return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
|
|
388
|
+
}
|
|
389
|
+
const requestedMs = normalizeTimeoutMs(timeout) ??
|
|
390
|
+
normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
|
|
391
|
+
hostCallTimeoutMs;
|
|
392
|
+
const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
393
|
+
// A trusted-pool write settles here — unknown if the call never returned an
|
|
371
394
|
// outcome.
|
|
372
395
|
const sending = {};
|
|
373
|
-
const outcome = yield* invocation
|
|
396
|
+
const outcome = yield* invocation
|
|
397
|
+
.pipeline(address, sentSecrets.redact(args ?? {}), invocationContext(sending, timeoutMs))
|
|
398
|
+
.pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
|
|
374
399
|
diagnostics?.recordCall(outcome);
|
|
375
400
|
return outcome.ok
|
|
376
|
-
? outcome.value
|
|
401
|
+
? { data: outcome.value, format: outcome.format }
|
|
377
402
|
: yield* Effect.fail(new InvocationFailure(outcome.error));
|
|
378
403
|
});
|
|
379
404
|
// Discovery gets the same treatment from here (L2): once the run has
|
|
@@ -386,21 +411,71 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
386
411
|
const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
|
|
387
412
|
const reading = Effect.tryPromise({
|
|
388
413
|
try: read,
|
|
389
|
-
catch: (err) => err instanceof DiscoveryPolicyError
|
|
390
|
-
? guestFailure(err.code, err.message)
|
|
391
|
-
: err,
|
|
414
|
+
catch: (err) => (err instanceof DiscoveryPolicyError ? guestFailure(err.code, err.message) : err),
|
|
392
415
|
});
|
|
393
|
-
return
|
|
416
|
+
return (!hostAccessSignal
|
|
394
417
|
? reading
|
|
395
|
-
:
|
|
418
|
+
: hostAccessSignal.aborted
|
|
396
419
|
? Effect.fail(cancelled())
|
|
397
|
-
: Effect.raceAllFirst([
|
|
398
|
-
reading,
|
|
399
|
-
fromSignal(signal).pipe(Effect.mapError(cancelled)),
|
|
400
|
-
])), Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
420
|
+
: 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))));
|
|
401
421
|
});
|
|
422
|
+
const meta = createMetaTools(registry, baseUrl, { trust: limits.trust, requestScope });
|
|
402
423
|
const operations = {
|
|
403
424
|
call,
|
|
425
|
+
result: (id, options, signal) => Effect.tryPromise({
|
|
426
|
+
try: async () => {
|
|
427
|
+
if (typeof id !== "string" ||
|
|
428
|
+
(!options && options !== undefined) ||
|
|
429
|
+
(options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
|
|
430
|
+
throw guestFailure("invalid_args", "Use connecta.result(id, { offset?, maxBytes? }).");
|
|
431
|
+
}
|
|
432
|
+
const args = (options ?? {});
|
|
433
|
+
if (Object.keys(args).some((key) => !["offset", "maxBytes"].includes(key)) ||
|
|
434
|
+
(args.offset !== undefined &&
|
|
435
|
+
(typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
|
|
436
|
+
(args.maxBytes !== undefined &&
|
|
437
|
+
(typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
|
|
438
|
+
throw guestFailure("invalid_args", "Use connecta.result(id, { offset?, maxBytes? }) with whole byte offsets >= 0 and maxBytes >= 1.");
|
|
439
|
+
}
|
|
440
|
+
const page = await meta.readResult({ id, ...args }, { signal: signal });
|
|
441
|
+
if (page.isError)
|
|
442
|
+
throw new InvocationFailure((page.structuredContent?.error ?? {
|
|
443
|
+
code: "not_found",
|
|
444
|
+
message: page.content[0]?.text ?? "Result unavailable.",
|
|
445
|
+
retryable: false,
|
|
446
|
+
}));
|
|
447
|
+
return page.structuredContent;
|
|
448
|
+
},
|
|
449
|
+
catch: (err) => err,
|
|
450
|
+
}),
|
|
451
|
+
skill: (name, _options, utilitySignal) => Effect.tryPromise({
|
|
452
|
+
try: async () => {
|
|
453
|
+
if (typeof name !== "string")
|
|
454
|
+
throw guestFailure("invalid_args", "Use connecta.skill(name) with an exact skill name.");
|
|
455
|
+
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
456
|
+
requestScope,
|
|
457
|
+
requestSignal: utilitySignal ?? hostAccessSignal,
|
|
458
|
+
probeTimeoutMs: limits.probeTimeoutMs,
|
|
459
|
+
defer: limits.defer,
|
|
460
|
+
});
|
|
461
|
+
return { name, format: "text", text: await skills.text(name) };
|
|
462
|
+
},
|
|
463
|
+
catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
|
|
464
|
+
}),
|
|
465
|
+
read: (uri) => Effect.suspend(() => {
|
|
466
|
+
const started = Date.now();
|
|
467
|
+
let connectorMs = 0;
|
|
468
|
+
return readResource(registry, uri, {
|
|
469
|
+
baseUrl,
|
|
470
|
+
requestScope,
|
|
471
|
+
sentSecrets,
|
|
472
|
+
timeoutMs: hostCallTimeoutMs,
|
|
473
|
+
onConnectorTime: (elapsed) => {
|
|
474
|
+
connectorMs += elapsed;
|
|
475
|
+
},
|
|
476
|
+
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
477
|
+
}).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordRead(Date.now() - started, connectorMs, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
478
|
+
}),
|
|
404
479
|
// Emission is a provider function, never an ExecuteResult field —
|
|
405
480
|
// that is what keeps the Executor contract untouched and parity
|
|
406
481
|
// structural (M8). It spends no host-call budget (M7); its own
|
|
@@ -410,7 +485,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
410
485
|
if (!limits.emitCollector) {
|
|
411
486
|
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
412
487
|
}
|
|
413
|
-
limits.emitCollector.accept(block);
|
|
488
|
+
limits.emitCollector.accept(sentSecrets.redact(block));
|
|
414
489
|
},
|
|
415
490
|
catch: (err) => err,
|
|
416
491
|
}),
|
|
@@ -418,6 +493,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
418
493
|
const args = (raw ?? {});
|
|
419
494
|
const result = flatSearchResult(await catalog.search({
|
|
420
495
|
...args,
|
|
496
|
+
includeSchemas: args.includeSchemas ?? "json",
|
|
421
497
|
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
422
498
|
}));
|
|
423
499
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
@@ -425,30 +501,90 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
425
501
|
}),
|
|
426
502
|
describe: (raw) => discovery("describe", async () => {
|
|
427
503
|
const args = (raw ?? {});
|
|
428
|
-
const result = { tools: await catalog.describe(args) };
|
|
504
|
+
const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
|
|
429
505
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
430
506
|
return result;
|
|
431
507
|
}),
|
|
432
508
|
};
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
const framed = (err) => Effect.suspend(() => {
|
|
509
|
+
// Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
|
|
510
|
+
// Anything that is not an InvocationFailure crosses unchanged.
|
|
511
|
+
const typedFailure = (err) => Effect.suspend(() => {
|
|
437
512
|
if (!(err instanceof InvocationFailure))
|
|
438
513
|
return Effect.fail(err);
|
|
439
|
-
const failure = boundedGuestFailure(err);
|
|
440
|
-
|
|
441
|
-
limits.onInvocationFailure?.(failure);
|
|
442
|
-
return Effect.fail(frame);
|
|
514
|
+
const failure = boundedGuestFailure(sentSecrets.redact(err));
|
|
515
|
+
return Effect.fail(failure);
|
|
443
516
|
});
|
|
444
517
|
return {
|
|
445
518
|
name: "connecta",
|
|
446
519
|
// Trusted host code the program runs after and cannot undo.
|
|
447
|
-
prelude:
|
|
520
|
+
prelude: guestPrelude(),
|
|
448
521
|
fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
|
|
449
522
|
name,
|
|
450
523
|
(...args) => {
|
|
451
|
-
|
|
524
|
+
if (budgetFailure)
|
|
525
|
+
return stopped;
|
|
526
|
+
// Invalid emits still allocate guest promises and typed rejections.
|
|
527
|
+
// Bound those attempts without consuming the invocation budget or discarding identities.
|
|
528
|
+
if (name === "emit" && ++emitAttempts > maxEmitAttempts) {
|
|
529
|
+
budgetFailure = guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`);
|
|
530
|
+
programWrites?.close();
|
|
531
|
+
limits.dispatchController?.abort();
|
|
532
|
+
limits.onHostCallBudgetExceeded?.(budgetFailure);
|
|
533
|
+
return stopped;
|
|
534
|
+
}
|
|
535
|
+
// L4/M7: spend on entry, before even invalid arguments are checked;
|
|
536
|
+
// emit has a separate budget and never spends this one.
|
|
537
|
+
const counted = name !== "emit";
|
|
538
|
+
if (counted && ++hostCalls.attempted > maxHostCalls) {
|
|
539
|
+
hostCalls.failed++;
|
|
540
|
+
budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
|
|
541
|
+
programWrites?.close();
|
|
542
|
+
limits.dispatchController?.abort();
|
|
543
|
+
limits.onHostCallBudgetExceeded?.(budgetFailure);
|
|
544
|
+
return stopped;
|
|
545
|
+
}
|
|
546
|
+
if (counted)
|
|
547
|
+
hostCalls.admitted++;
|
|
548
|
+
const started = Date.now();
|
|
549
|
+
const utility = name === "result" || name === "skill";
|
|
550
|
+
const invoke = Effect.suspend(() => operation(...args));
|
|
551
|
+
const guarded = utility
|
|
552
|
+
? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal)), {
|
|
553
|
+
timeoutMs: hostCallTimeoutMs,
|
|
554
|
+
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
555
|
+
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
556
|
+
})
|
|
557
|
+
: invoke;
|
|
558
|
+
const settled = runEdge(guarded.pipe(Effect.catch((error) => {
|
|
559
|
+
if (utility && hostAccessSignal?.aborted)
|
|
560
|
+
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
561
|
+
if (utility && error instanceof InvocationFailure && error.code === "timeout") {
|
|
562
|
+
const elapsedMs = Date.now() - started;
|
|
563
|
+
const message = `Operation "connecta.${name}" timed out during ${name === "result" ? "result storage" : "skill lookup"} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
|
|
564
|
+
return typedFailure(new InvocationFailure({
|
|
565
|
+
code: "timeout",
|
|
566
|
+
message,
|
|
567
|
+
retryable: true,
|
|
568
|
+
details: {
|
|
569
|
+
operation: `connecta.${name}`,
|
|
570
|
+
stage: name === "result" ? "result storage" : "skill lookup",
|
|
571
|
+
elapsedMs,
|
|
572
|
+
deadlineMs: hostCallTimeoutMs,
|
|
573
|
+
},
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
return typedFailure(error);
|
|
577
|
+
}))).then((value) => {
|
|
578
|
+
if (counted)
|
|
579
|
+
hostCalls.succeeded++;
|
|
580
|
+
return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
|
|
581
|
+
}, (err) => {
|
|
582
|
+
if (counted)
|
|
583
|
+
hostCalls.failed++;
|
|
584
|
+
if (budgetFailure)
|
|
585
|
+
return stopped;
|
|
586
|
+
throw redactAgentOutput(sentSecrets, err);
|
|
587
|
+
});
|
|
452
588
|
// The executor owns this promise, and a program may abandon a call
|
|
453
589
|
// that the run's end then cancels before anyone has awaited it.
|
|
454
590
|
// The rejection is still there for whoever does; it is just not an
|
|
@@ -476,11 +612,12 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
|
|
|
476
612
|
* ceiling as an untyped executor failure naming the key that sets it. Either
|
|
477
613
|
* way the handler returns and releases the lease, and the abandoned call
|
|
478
614
|
* 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
|
|
615
|
+
* the Worker adapter disposes its handles without waiting for the guest. A
|
|
616
|
+
* lease granted after its request gave up goes to `late`. The result contract is untouched
|
|
481
617
|
* — this only decides when to stop waiting for one.
|
|
482
618
|
*/
|
|
483
619
|
function awaitExecutor(start, signal, options = {}) {
|
|
620
|
+
const started = Date.now();
|
|
484
621
|
const contenders = [
|
|
485
622
|
Effect.suspend(() => {
|
|
486
623
|
let pending;
|
|
@@ -501,12 +638,21 @@ function awaitExecutor(start, signal, options = {}) {
|
|
|
501
638
|
// which the executor's promise chain can still lose to.)
|
|
502
639
|
fromSignal(signal).pipe(Effect.catch(() => Effect.sleep(Duration.millis(1))), Effect.andThen(Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled.")))),
|
|
503
640
|
];
|
|
641
|
+
if (options.terminal)
|
|
642
|
+
contenders.push(options.terminal);
|
|
504
643
|
const { watchdog } = options;
|
|
505
644
|
if (watchdog) {
|
|
506
645
|
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
|
-
|
|
646
|
+
watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
|
|
647
|
+
watchdogMs: watchdog.ms,
|
|
648
|
+
});
|
|
649
|
+
const elapsedMs = Date.now() - started;
|
|
650
|
+
return Effect.fail(new InvocationFailure({
|
|
651
|
+
code: "timeout",
|
|
652
|
+
message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${watchdog.ms}ms); sandbox unresponsive.`,
|
|
653
|
+
retryable: false,
|
|
654
|
+
details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs: watchdog.ms },
|
|
655
|
+
}));
|
|
510
656
|
}))));
|
|
511
657
|
}
|
|
512
658
|
return Effect.raceAllFirst(contenders);
|
|
@@ -540,44 +686,39 @@ function runSignal(caller) {
|
|
|
540
686
|
* which workerd refuses for any I/O. Awaiting the promise resumes it here.
|
|
541
687
|
*/
|
|
542
688
|
function leased(executor, signal) {
|
|
543
|
-
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal }), signal, {
|
|
689
|
+
return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
|
|
544
690
|
late: (lease) => lease.release(),
|
|
545
691
|
}), (lease) => Effect.sync(() => lease.release()));
|
|
546
692
|
}
|
|
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. */
|
|
693
|
+
/** The execute_code handler. Exported for direct testing. */
|
|
560
694
|
export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
|
|
561
695
|
const watchdog = {
|
|
562
696
|
ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
|
|
563
697
|
logger,
|
|
564
698
|
};
|
|
565
699
|
const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
|
|
566
|
-
const play = (program, diagnosticsRequested, callerSignal) => Effect.suspend(() => {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
: undefined;
|
|
700
|
+
const play = (program, diagnosticsRequested, callerSignal, requestScope) => Effect.suspend(() => {
|
|
701
|
+
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
702
|
+
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
570
703
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
571
|
-
const
|
|
572
|
-
const
|
|
704
|
+
const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
705
|
+
const programWrites = new ProgramWrites();
|
|
706
|
+
const dispatchController = new AbortController();
|
|
707
|
+
const terminal = Deferred.makeUnsafe();
|
|
708
|
+
let budgetFailure;
|
|
709
|
+
let executorLogs;
|
|
573
710
|
// The run's scope holds its signal and its lease. However the run
|
|
574
711
|
// ends — a result, a thrown executor, the watchdog, cancellation —
|
|
575
712
|
// closing it releases the lease and then aborts the signal, so
|
|
576
713
|
// nothing the run started outlives the request.
|
|
577
714
|
const run = Effect.gen(function* () {
|
|
715
|
+
// Per-call signals cancel individual fetches, but do not close the
|
|
716
|
+
// shared transport. Close its scope after the run's signal ends, on
|
|
717
|
+
// every exit, using the existing bounded cleanup and deferred tail.
|
|
718
|
+
yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
|
|
578
719
|
const signal = yield* runSignal(callerSignal);
|
|
579
720
|
// Admission comes before provider construction: queued calls retain
|
|
580
|
-
// no catalogs
|
|
721
|
+
// no catalogs or provider closures.
|
|
581
722
|
let lease;
|
|
582
723
|
if (isAdmittingExecutor(executor)) {
|
|
583
724
|
lease = yield* timed((elapsed) => {
|
|
@@ -594,11 +735,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
594
735
|
if (diagnostics)
|
|
595
736
|
diagnostics.setupMs = elapsed;
|
|
596
737
|
}, Effect.sync(() => sandboxProvider(registry, baseUrl, activity, {
|
|
738
|
+
sentSecrets,
|
|
597
739
|
signal,
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
740
|
+
hostCalls,
|
|
741
|
+
onHostCallBudgetExceeded: (failure) => {
|
|
742
|
+
budgetFailure = failure;
|
|
743
|
+
Deferred.doneUnsafe(terminal, Effect.fail(failure));
|
|
602
744
|
},
|
|
603
745
|
emitCollector: emitted,
|
|
604
746
|
...(diagnostics ? { diagnostics } : {}),
|
|
@@ -606,62 +748,105 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
606
748
|
probeTimeoutMs: config.probeTimeoutMs,
|
|
607
749
|
maxHostCalls: config.maxHostCalls,
|
|
608
750
|
hostCallTimeoutMs,
|
|
751
|
+
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
752
|
+
programDeadlineAt: Date.now() + watchdog.ms,
|
|
609
753
|
defer: config.defer,
|
|
610
|
-
|
|
754
|
+
trust: config.trust,
|
|
611
755
|
maxWrites: config.maxWrites,
|
|
612
|
-
|
|
613
|
-
|
|
756
|
+
programWrites,
|
|
757
|
+
dispatchController,
|
|
758
|
+
}, requestScope)));
|
|
614
759
|
if (signal.aborted) {
|
|
615
760
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
616
761
|
}
|
|
617
762
|
const admitted = lease;
|
|
618
|
-
//
|
|
763
|
+
// A trusted-pool write may still be on the wire when the program settles:
|
|
619
764
|
// close, so a write gated after this is not sent, then let the ones
|
|
620
765
|
// already dispatched finish before the scope aborts them — an
|
|
621
766
|
// abandoned write would be one whose outcome nobody knows.
|
|
622
767
|
return yield* timed((elapsed) => {
|
|
623
768
|
if (diagnostics)
|
|
624
769
|
diagnostics.executorWallMs = elapsed;
|
|
625
|
-
}, awaitExecutor(() => admitted
|
|
626
|
-
? admitted.execute(program, [provider])
|
|
627
|
-
: executor.execute(program, [provider])
|
|
628
|
-
|
|
629
|
-
return
|
|
770
|
+
}, awaitExecutor(() => (admitted
|
|
771
|
+
? admitted.execute(wrapGuestProgram(program), [provider])
|
|
772
|
+
: executor.execute(wrapGuestProgram(program), [provider])).then((outcome) => {
|
|
773
|
+
executorLogs = sentSecrets.redact(outcome?.logs);
|
|
774
|
+
return sentSecrets.redact(outcome);
|
|
775
|
+
}, (err) => {
|
|
776
|
+
if (err !== null && typeof err === "object" && "logs" in err) {
|
|
777
|
+
executorLogs = sentSecrets.redact(err.logs);
|
|
778
|
+
}
|
|
779
|
+
throw sentSecrets.redact(err);
|
|
780
|
+
}), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
|
|
781
|
+
programWrites.close();
|
|
782
|
+
dispatchController.abort();
|
|
783
|
+
return programWrites.drain();
|
|
630
784
|
}))));
|
|
631
785
|
});
|
|
632
|
-
const reported = { emitted, diagnostics,
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
786
|
+
const reported = { emitted, diagnostics, program };
|
|
787
|
+
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
788
|
+
// Releasing a QuickJS lease ends its child and rejects execute() with
|
|
789
|
+
// the retained log prefix. Give that report one timer turn, just as
|
|
790
|
+
// cancellation does in awaitExecutor; never wait on a Worker isolate.
|
|
791
|
+
budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
|
|
792
|
+
return Effect.map(Effect.map(exited, (exit) => {
|
|
793
|
+
// A synchronous fire-and-forget burst can also settle its executor in
|
|
794
|
+
// this turn. The host's terminal refusal always wins over that value.
|
|
795
|
+
if (budgetFailure instanceof HostCallBudgetExceeded) {
|
|
796
|
+
const failed = failureResponse(budgetFailure.details.message, {
|
|
797
|
+
code: budgetFailure.details,
|
|
798
|
+
logs: executeLogs(executorLogs),
|
|
799
|
+
emitted,
|
|
800
|
+
diagnostics,
|
|
642
801
|
});
|
|
802
|
+
return failed;
|
|
643
803
|
}
|
|
644
|
-
|
|
645
|
-
|
|
804
|
+
if (Exit.isFailure(exit)) {
|
|
805
|
+
return failedRun(sentSecrets.redact(Cause.squash(exit.cause)), logger, reported);
|
|
806
|
+
}
|
|
807
|
+
return finishedRun(sentSecrets.redact(exit.value), reported);
|
|
808
|
+
}), (unfinished) => {
|
|
809
|
+
// Every run response passes through write accounting.
|
|
810
|
+
const response = programWrites.finish(unfinished);
|
|
811
|
+
// Calls abandoned by the guest are failed when the run cancels them.
|
|
812
|
+
const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
|
|
813
|
+
response.structuredContent = { ...response.structuredContent, hostCalls: counts };
|
|
814
|
+
if (response.content[0]?.type === "text")
|
|
815
|
+
response.content[0].text = JSON.stringify(response.structuredContent);
|
|
816
|
+
return response;
|
|
817
|
+
}).pipe(Effect.map((result) => redactAgentOutput(sentSecrets, result)));
|
|
646
818
|
});
|
|
647
|
-
return (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
code: { code: "invalid_args", message, retryable: false }
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
819
|
+
return agentOutputOperations((requestScope) => ({
|
|
820
|
+
execute: ({ code, diagnostics }, options = {}) => {
|
|
821
|
+
// A code-unit count above the cap is already too large in UTF-8. Check
|
|
822
|
+
// that first so a huge direct-call string is never encoded in full.
|
|
823
|
+
if (code.length > EXECUTE_MAX_CODE_BYTES ||
|
|
824
|
+
new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
|
|
825
|
+
const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
|
|
826
|
+
const response = failureResponse(message, { code: { code: "invalid_args", message, retryable: false } });
|
|
827
|
+
response.structuredContent = {
|
|
828
|
+
...response.structuredContent,
|
|
829
|
+
hostCalls: { attempted: 0, admitted: 0, succeeded: 0, failed: 0 },
|
|
830
|
+
};
|
|
831
|
+
response.content[0].text = JSON.stringify(response.structuredContent);
|
|
832
|
+
return Promise.resolve(response);
|
|
833
|
+
}
|
|
834
|
+
return runEdge(Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal, requestScope)));
|
|
835
|
+
},
|
|
836
|
+
}), config.requestScope).execute;
|
|
659
837
|
}
|
|
660
838
|
/** 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
|
-
:
|
|
839
|
+
function failedRun(err, logger, { emitted, diagnostics, program }) {
|
|
840
|
+
const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
|
|
841
|
+
if (err instanceof InvocationFailure)
|
|
842
|
+
return failureResponse(err.details.message, { logs, emitted, diagnostics, code: err.details });
|
|
843
|
+
if (err instanceof SyntaxError)
|
|
844
|
+
return failureResponse(err.message, {
|
|
845
|
+
logs,
|
|
846
|
+
emitted,
|
|
847
|
+
diagnostics,
|
|
848
|
+
code: programError({ name: "SyntaxError", message: err.message }, program),
|
|
849
|
+
});
|
|
665
850
|
if (err instanceof ExecutorAdmissionError) {
|
|
666
851
|
if (err.code === "executor_overloaded") {
|
|
667
852
|
logger.warn("[connecta] execute_code admission rejected", {
|
|
@@ -677,9 +862,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
|
|
|
677
862
|
code: err.code,
|
|
678
863
|
message: err.message,
|
|
679
864
|
retryable: err.retryable,
|
|
680
|
-
...(err.retryAfterMs !== undefined
|
|
681
|
-
? { retryAfterMs: err.retryAfterMs }
|
|
682
|
-
: {}),
|
|
865
|
+
...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
|
|
683
866
|
},
|
|
684
867
|
});
|
|
685
868
|
}
|
|
@@ -691,7 +874,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
|
|
|
691
874
|
});
|
|
692
875
|
}
|
|
693
876
|
/** The response for an ExecuteResult: the program's error or its value. */
|
|
694
|
-
function finishedRun(outcome, { emitted, diagnostics,
|
|
877
|
+
function finishedRun(outcome, { emitted, diagnostics, program }) {
|
|
695
878
|
if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
|
|
696
879
|
return failureResponse("Executor failed: expected an ExecuteResult object.", {
|
|
697
880
|
emitted,
|
|
@@ -709,52 +892,32 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
|
|
|
709
892
|
});
|
|
710
893
|
}
|
|
711
894
|
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, {
|
|
895
|
+
const failure = outcome.failure?.call;
|
|
896
|
+
if (outcome.failure?.timeout) {
|
|
897
|
+
const { elapsedMs, deadlineMs } = outcome.failure.timeout;
|
|
898
|
+
const message = `Operation "execute_code" timed out during sandbox after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`;
|
|
899
|
+
return failureResponse(message, {
|
|
746
900
|
logs,
|
|
747
901
|
emitted,
|
|
748
902
|
diagnostics,
|
|
749
|
-
code:
|
|
903
|
+
code: {
|
|
904
|
+
code: "timeout",
|
|
905
|
+
message,
|
|
906
|
+
retryable: false,
|
|
907
|
+
details: { operation: "execute_code", stage: "sandbox", elapsedMs, deadlineMs },
|
|
908
|
+
},
|
|
750
909
|
});
|
|
751
910
|
}
|
|
752
|
-
|
|
753
|
-
return failureResponse(message, {
|
|
911
|
+
return failureResponse(failure?.message ?? outcome.error, {
|
|
754
912
|
logs,
|
|
755
913
|
emitted,
|
|
756
914
|
diagnostics,
|
|
757
|
-
code:
|
|
915
|
+
code: failure ??
|
|
916
|
+
programError({
|
|
917
|
+
name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
|
|
918
|
+
message: outcome.error || "Execution failed without an error message.",
|
|
919
|
+
...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
|
|
920
|
+
}, program),
|
|
758
921
|
});
|
|
759
922
|
}
|
|
760
923
|
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
@@ -762,7 +925,7 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
|
|
|
762
925
|
// error path so captured logs survive instead of a raw SDK 500.
|
|
763
926
|
let result;
|
|
764
927
|
try {
|
|
765
|
-
result = guardExecuteResultValue(outcome.result);
|
|
928
|
+
result = guardExecuteResultValue(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
|
|
766
929
|
}
|
|
767
930
|
catch (err) {
|
|
768
931
|
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
@@ -794,33 +957,20 @@ function executeLogs(value) {
|
|
|
794
957
|
}
|
|
795
958
|
function failureResponse(message, options) {
|
|
796
959
|
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) : ""}`);
|
|
960
|
+
const result = jsonResult({
|
|
961
|
+
error: typeof code === "string" ? { code, message, retryable: false } : code,
|
|
962
|
+
...(logs ? { logs } : {}),
|
|
963
|
+
...(emitted ? discardedEmits(emitted) : {}),
|
|
964
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
965
|
+
});
|
|
966
|
+
result.isError = true;
|
|
967
|
+
return result;
|
|
810
968
|
}
|
|
811
969
|
/**
|
|
812
970
|
* A failed program delivers no blocks and reports how many were discarded.
|
|
813
971
|
*/
|
|
814
972
|
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
|
-
: "";
|
|
973
|
+
return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
|
|
824
974
|
}
|
|
825
975
|
function connectorInventory(connectors) {
|
|
826
976
|
const prefix = "Connectors: ";
|
|
@@ -829,14 +979,10 @@ function connectorInventory(connectors) {
|
|
|
829
979
|
const entries = connectors.map((connector) => {
|
|
830
980
|
const address = connector.id;
|
|
831
981
|
const title = connector.title?.replace(/\s+/g, " ").trim();
|
|
832
|
-
const label = title && title !== connector.id
|
|
833
|
-
? `${address}: ${boundedEchoText(title, 45)}`
|
|
834
|
-
: address;
|
|
982
|
+
const label = title && title !== connector.id ? `${address}: ${boundedEchoText(title, 45)}` : address;
|
|
835
983
|
if (!connectorGuide(connector))
|
|
836
984
|
return label;
|
|
837
|
-
const requirement = connectorGuideRequired(connector)
|
|
838
|
-
? "required guide"
|
|
839
|
-
: "guide";
|
|
985
|
+
const requirement = connectorGuideRequired(connector) ? "required guide" : "guide";
|
|
840
986
|
return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
|
|
841
987
|
});
|
|
842
988
|
const shown = [];
|
|
@@ -858,29 +1004,26 @@ function connectorInventory(connectors) {
|
|
|
858
1004
|
return `${prefix}${shown.join(", ")}.`;
|
|
859
1005
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
860
1006
|
}
|
|
861
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites) => `
|
|
862
|
-
|
|
1007
|
+
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
1008
|
${connectorInventory(connectors)}
|
|
864
1009
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1010
|
+
Use async () => { ... }, no arguments, with the connecta global. No portable ambient capabilities. API notation:
|
|
1011
|
+
\`\`\`ts
|
|
1012
|
+
declare const connecta: {
|
|
1013
|
+
${GUEST_API_DECLARATION}
|
|
1014
|
+
};
|
|
1015
|
+
\`\`\`
|
|
1016
|
+
Discovery: { tools }; check search catalogErrors/absence. JSON schemas default; compact is text. GuestResult is { data, format: "json" | "text" }; inspect format before fields. Pages: text, hasMore, nextOffset in UTF-8 bytes. Promise.allSettled keeps typed failures. Host-call budget_exceeded is terminal, even through catch/allSettled. Runs report hostCalls.
|
|
871
1017
|
|
|
872
|
-
|
|
873
|
-
// Module scope, like the other
|
|
1018
|
+
Reduce JSON. Never repeat writes for output. emit: text or base64 image/audio, safe without await, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares the result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
|
|
1019
|
+
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
874
1020
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
875
1021
|
// view, so they live in the description, never here.
|
|
876
1022
|
const EXECUTE_INPUT = advertisedSchema(z.object({
|
|
877
1023
|
code: z
|
|
878
1024
|
.string()
|
|
879
1025
|
.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."),
|
|
1026
|
+
diagnostics: z.boolean().optional().describe("Add request-local, payload-free timing and result-size summaries."),
|
|
884
1027
|
}));
|
|
885
1028
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
886
1029
|
export function registerExecuteTool(server, registry, ctx) {
|
|
@@ -899,6 +1042,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
899
1042
|
const connectors = registry.listConnectors();
|
|
900
1043
|
const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
|
|
901
1044
|
const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
|
|
1045
|
+
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
902
1046
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
903
1047
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
904
1048
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
@@ -907,25 +1051,29 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
907
1051
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
908
1052
|
watchdogMs: ctx.watchdogMs,
|
|
909
1053
|
defer: ctx.defer,
|
|
910
|
-
|
|
1054
|
+
trust: ctx.trust,
|
|
911
1055
|
maxWrites,
|
|
1056
|
+
requestScope: ctx.requestScope,
|
|
912
1057
|
});
|
|
913
1058
|
server.registerTool("execute_code", {
|
|
914
|
-
description: executeDescription(emitBudgets,
|
|
1059
|
+
description: executeDescription(emitBudgets, {
|
|
1060
|
+
...hostLimits,
|
|
1061
|
+
hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1062
|
+
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.trust ?? "read-only"),
|
|
915
1063
|
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.
|
|
1064
|
+
outputSchema: EXECUTE_OUTPUT,
|
|
1065
|
+
// The host sees execute_code as a write only on a trusted endpoint.
|
|
923
1066
|
annotations: {
|
|
924
|
-
readOnlyHint:
|
|
925
|
-
destructiveHint:
|
|
1067
|
+
readOnlyHint: ctx.trust !== "trusted",
|
|
1068
|
+
destructiveHint: ctx.trust === "trusted",
|
|
926
1069
|
openWorldHint: true,
|
|
927
1070
|
},
|
|
928
|
-
}, (args, extra) =>
|
|
929
|
-
|
|
930
|
-
|
|
1071
|
+
}, (args, extra) => {
|
|
1072
|
+
if (ctx.client)
|
|
1073
|
+
bindMcpClient(extra, ctx.client, ctx.activity);
|
|
1074
|
+
const run = () => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
|
|
1075
|
+
signal,
|
|
1076
|
+
}));
|
|
1077
|
+
return ctx.authElicitation ? ctx.authElicitation.run("execute_code", args, extra, run) : run();
|
|
1078
|
+
});
|
|
931
1079
|
}
|