@zackbart/connecta 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +10 -8
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/meta-tools.js
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { operationActivity } from "./operation-activity.js";
|
|
3
|
+
import { SkillLookupError } from "./skills.js";
|
|
4
|
+
import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
|
|
1
5
|
import { META_ROUTES } from "./agent-routes.js";
|
|
2
6
|
import { toolError } from "./tool-error.js";
|
|
3
7
|
import { upwardStep } from "./upward.js";
|
|
4
8
|
import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
|
|
5
9
|
import { registryProgram } from "./runtime/registry.js";
|
|
6
|
-
import { runEdge } from "./runtime/run.js";
|
|
10
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
7
11
|
import { authorizationHandoff, authorizeConnector, bindAuthorizationHandoffs } from "./authorization-handoff.js";
|
|
8
12
|
import { bindMcpClient } from "./mcp-client-context.js";
|
|
9
13
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
10
14
|
import * as z from "zod";
|
|
11
15
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
12
|
-
import { boundedDiscoveryText, CatalogService,
|
|
16
|
+
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
13
17
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
14
|
-
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
18
|
+
import { boundedEchoText, clipText, ConnectorCallError, MAX_TOOL_ERROR_BYTES, msg, } from "./errors.js";
|
|
15
19
|
import { failureRecord, logFailure } from "./operator-record.js";
|
|
20
|
+
import { RESULT_ENVELOPE_V4 } from "./result-seal.js";
|
|
16
21
|
import { guardExecuteResultValue, MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
|
|
17
22
|
import { InvocationService } from "./invocation.js";
|
|
18
23
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
19
24
|
import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
|
|
20
25
|
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
|
|
21
|
-
import {
|
|
26
|
+
import { isWriteLike } from "./tool-safety.js";
|
|
27
|
+
import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT } from "./meta-output.js";
|
|
22
28
|
import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
|
|
23
29
|
import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
|
|
24
30
|
import { captureDownstreamInput } from "./downstream-input.js";
|
|
25
31
|
import { PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
|
|
32
|
+
import { previewResult } from "./result-preview.js";
|
|
26
33
|
export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
|
|
27
34
|
const enc = new TextEncoder();
|
|
28
35
|
// Each page is a slice of payload bytes, so a leading U+FEFF is data.
|
|
@@ -38,6 +45,9 @@ export function jsonResult(obj, text = JSON.stringify(obj)) {
|
|
|
38
45
|
function errorResult(message) {
|
|
39
46
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
40
47
|
}
|
|
48
|
+
function resultError(message) {
|
|
49
|
+
return toolError(utilityRecovery("invalid_args", message));
|
|
50
|
+
}
|
|
41
51
|
function discoveryErrorResult(error) {
|
|
42
52
|
return toolError({ code: error.code, message: error.message, retryable: false });
|
|
43
53
|
}
|
|
@@ -110,8 +120,8 @@ function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
110
120
|
return e;
|
|
111
121
|
}
|
|
112
122
|
/** Every readable stash carries host-admitted bindings; older entries fail closed. */
|
|
113
|
-
const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
|
|
114
123
|
const RESULT_ENVELOPE_V4_HEADER = /^connecta-result-v4:([A-Za-z0-9+/=]+):(\d+):(\d+):(\d+):/;
|
|
124
|
+
const RESULT_REFUSED_V4_HEADER = /^connecta-result-refused-v4:([A-Za-z0-9+/=]+):$/;
|
|
115
125
|
/**
|
|
116
126
|
* Smallest chunk of result text stored under one key. A multiple of three so
|
|
117
127
|
* every chunk's base64 stands alone and a byte offset inside it lands on a
|
|
@@ -131,6 +141,30 @@ const RESULT_MAX_CHUNKS = 32;
|
|
|
131
141
|
function resultChunkBytes(totalBytes) {
|
|
132
142
|
return Math.max(RESULT_CHUNK_BYTES, Math.ceil(totalBytes / RESULT_MAX_CHUNKS / 3) * 3);
|
|
133
143
|
}
|
|
144
|
+
const isBinary = (item) => ArrayBuffer.isView(item) || item instanceof ArrayBuffer;
|
|
145
|
+
/**
|
|
146
|
+
* JSON text, and whether a binary value went into it: as written (the holder's value, before
|
|
147
|
+
* any toJSON) or as a toJSON returned it. Whatever JSON makes of one, redaction checks its bytes
|
|
148
|
+
* only while they are binary. Text results have no binary values to lose.
|
|
149
|
+
*/
|
|
150
|
+
function serializedWithBinary(value) {
|
|
151
|
+
let binary = false;
|
|
152
|
+
const text = JSON.stringify(value, function (key, item) {
|
|
153
|
+
binary ||= isBinary(this[key]) || isBinary(item);
|
|
154
|
+
return item;
|
|
155
|
+
});
|
|
156
|
+
return { text: text === undefined ? String(value) : text, binary };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Give back a stash nothing can name any more; its layout follows from its size. Sealing
|
|
160
|
+
* writes a header for whatever the ledger still holds, so the id leaves the ledger first.
|
|
161
|
+
* Once sealing has begun it owns the entry, which keeps its charge until it expires.
|
|
162
|
+
*/
|
|
163
|
+
export function releaseStashedResult(registry, held, id, totalBytes) {
|
|
164
|
+
return Effect.suspend(() => held && !held.discard(id)
|
|
165
|
+
? Effect.void
|
|
166
|
+
: registryProgram(registry, "releaseResult", id, Math.max(1, Math.ceil(totalBytes / resultChunkBytes(totalBytes)))));
|
|
167
|
+
}
|
|
134
168
|
/**
|
|
135
169
|
* Base64 of `bytes`, in three-byte-aligned batches so the argument list of one
|
|
136
170
|
* spread never grows with the result. Alignment matters: an unaligned batch
|
|
@@ -143,6 +177,26 @@ function base64Of(bytes) {
|
|
|
143
177
|
}
|
|
144
178
|
return out;
|
|
145
179
|
}
|
|
180
|
+
/** Calls a whole-result loop leaves the program, so it can also act on what it finds. */
|
|
181
|
+
const LOOP_SPARE_HOST_CALLS = 2;
|
|
182
|
+
/**
|
|
183
|
+
* Host calls to read every page of a retained result at the default page size.
|
|
184
|
+
* A page can stop up to three bytes short to keep a character whole, so the
|
|
185
|
+
* count assumes every one does.
|
|
186
|
+
*/
|
|
187
|
+
const resultPages = (cap, totalBytes) => Math.ceil(totalBytes / Math.max(1, Math.min(cap, PROGRAM_RESULT_PAGE_BYTES) - 3));
|
|
188
|
+
/** Whether a program with this many calls left can walk every page of a retained result. */
|
|
189
|
+
const loopFits = (cap, totalBytes, calls) => resultPages(cap, totalBytes) + LOOP_SPARE_HOST_CALLS <= calls;
|
|
190
|
+
/**
|
|
191
|
+
* A read handle's advice, sized to the calls its program has left. Overrunning
|
|
192
|
+
* them ends the run and discards everything it read, so a result that can't fit
|
|
193
|
+
* pages across programs, which carry the offset and reduction between them.
|
|
194
|
+
*/
|
|
195
|
+
function readHandleHint(id, cap, totalBytes, calls) {
|
|
196
|
+
if (loopFits(cap, totalBytes, calls.left))
|
|
197
|
+
return "Loop connecta.result(handle, { offset }) until !hasMore, setting offset to nextOffset.";
|
|
198
|
+
return `Needs up to ${resultPages(cap, totalBytes)} host calls; this program has ${calls.left} left, and overrunning returns nothing. Call connecta.result(${JSON.stringify(id)}, { offset }) at most ${Math.max(0, calls.left - LOOP_SPARE_HOST_CALLS)} times here and ${Math.max(1, calls.max - LOOP_SPARE_HOST_CALLS)} per new program, return your reduction and nextOffset, and continue from both within ${RESULT_TTL_SECONDS / 60} minutes.`;
|
|
199
|
+
}
|
|
146
200
|
/**
|
|
147
201
|
* The page a notice's nextAction asks a program to read: it fits the program's
|
|
148
202
|
* return even if every byte escapes to six JSON characters, with room for the
|
|
@@ -152,6 +206,28 @@ function programPageBytes(results) {
|
|
|
152
206
|
const fits = Math.floor(((results.programChars ?? MAX_EXECUTE_RESULT_CHARS) - 1024) / 6);
|
|
153
207
|
return Math.max(4, Math.min(fits, results.cap, PROGRAM_RESULT_PAGE_BYTES));
|
|
154
208
|
}
|
|
209
|
+
/** A retained result its run could not seal: a typed answer, and repeating this page cannot help. */
|
|
210
|
+
function resultNotRetained(write, id) {
|
|
211
|
+
const label = `"${boundedEchoText(id, 96)}"`;
|
|
212
|
+
return write
|
|
213
|
+
? {
|
|
214
|
+
code: "result_not_retained",
|
|
215
|
+
message: `Result ${label} is not retrievable: it was not retained past the run that produced it. ${WRITE_ALREADY_RAN}`,
|
|
216
|
+
retryable: false,
|
|
217
|
+
nextAction: {
|
|
218
|
+
action: "recover_result",
|
|
219
|
+
purpose: "Read the write's target for its outcome, or report the missing output. Never repeat the write.",
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
: {
|
|
223
|
+
code: "result_not_retained",
|
|
224
|
+
message: `Result ${label} was not retained past the run that produced it. Rerun the read for a fresh result.`,
|
|
225
|
+
retryable: false,
|
|
226
|
+
nextAction: { action: "recover_result", purpose: "Rerun the read in a new call or program." },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** nextAction for a whole read: runs as written, and is meant to be edited first. */
|
|
230
|
+
const reduceWhole = (id, results) => `async () => { const { data } = await connecta.result(${JSON.stringify(id)}, { all: true }); return /* reduce data to what you need */ JSON.stringify(data).slice(0, ${programPageBytes(results)}); }`;
|
|
155
231
|
/** Opens a write's notice, ahead of any paging instruction. */
|
|
156
232
|
const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
|
|
157
233
|
/** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
|
|
@@ -180,7 +256,9 @@ function isWellFormedText(text) {
|
|
|
180
256
|
* it is the one thing its notice forbids.
|
|
181
257
|
*/
|
|
182
258
|
const readReduceFirst = (routes) => `To find something specific, repeat this read inside ${routes.program} with connecta.call and filter or search the result there, returning only what matters`;
|
|
183
|
-
function pagingHint(results, totalBytes, preview) {
|
|
259
|
+
function pagingHint(results, id, totalBytes, preview) {
|
|
260
|
+
if (results.readBytes !== undefined && totalBytes <= results.readBytes)
|
|
261
|
+
return `${results.write ? WRITE_ALREADY_RAN + " " : ""}Read this completed return with connecta.result(id, { all: true }) inside ${results.routes.program} and reduce it, as nextAction does.`;
|
|
184
262
|
if (results.pagingTool === "read_result") {
|
|
185
263
|
const shown = preview.kind === "prefix"
|
|
186
264
|
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest`
|
|
@@ -197,6 +275,11 @@ function pagingHint(results, totalBytes, preview) {
|
|
|
197
275
|
: preview.kind === "text-of-envelope"
|
|
198
276
|
? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
|
|
199
277
|
: [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
|
|
278
|
+
// Stop fetching again when one program can reach every retained byte: the reduction
|
|
279
|
+
// still sees the whole value, and the downstream is asked only once. Beyond that the
|
|
280
|
+
// loop would end in budget_exceeded with nothing returned, so a program repeats the read.
|
|
281
|
+
if (loopFits(results.cap, totalBytes, results.programHostCalls ?? CONFIG_DEFAULTS.execute.maxHostCalls))
|
|
282
|
+
return `${shown} The full result is retained for ${RESULT_TTL_SECONDS / 60} minutes. To find something in it, loop connecta.result(${JSON.stringify(id)}, { offset }) from offset 0 until !hasMore, setting offset to nextOffset, in one ${results.routes.program} program; filter there and return the matches. To read on, run nextAction.`;
|
|
200
283
|
return `${shown} ${readReduceFirst(results.routes)}; ${full} with connecta.result using nextAction.`;
|
|
201
284
|
}
|
|
202
285
|
const body = preview.kind === "prefix"
|
|
@@ -236,7 +319,8 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
236
319
|
// reader below maps a byte offset back to chunk index and base64 quad.
|
|
237
320
|
const chunkBytes = resultChunkBytes(totalBytes);
|
|
238
321
|
const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
|
|
239
|
-
const
|
|
322
|
+
const head = `${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`;
|
|
323
|
+
const chunks = [head];
|
|
240
324
|
for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
|
|
241
325
|
const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
|
|
242
326
|
if (offset === 0)
|
|
@@ -244,9 +328,30 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
244
328
|
else
|
|
245
329
|
chunks.push(chunk);
|
|
246
330
|
}
|
|
247
|
-
|
|
331
|
+
const held = results.held;
|
|
332
|
+
// Taken before the booking, so a held header can never outlive the chunks it describes.
|
|
333
|
+
const expiresAtMs = Date.now() + RESULT_TTL_SECONDS * 1000;
|
|
334
|
+
if (held && !held.results.accepts())
|
|
335
|
+
throw new Error("The run already sealed its results");
|
|
336
|
+
const stashed = yield* upwardStep(authInvoke(results, "set", [id, chunks, RESULT_TTL_SECONDS, ...(held ? [{ holdFirst: true }] : [])]));
|
|
337
|
+
if (!stashed)
|
|
248
338
|
throw new Error("Result stash capacity exhausted");
|
|
249
|
-
|
|
339
|
+
if (held &&
|
|
340
|
+
!held.results.add({
|
|
341
|
+
id,
|
|
342
|
+
binding,
|
|
343
|
+
total: totalBytes,
|
|
344
|
+
chunkBytes,
|
|
345
|
+
cap: results.cap,
|
|
346
|
+
first: chunks[0].slice(head.length),
|
|
347
|
+
chunks: chunks.length,
|
|
348
|
+
passed: held.passed,
|
|
349
|
+
format: held.format,
|
|
350
|
+
binary: held.binary,
|
|
351
|
+
write: results.binding.classification === "write",
|
|
352
|
+
expiresAtMs,
|
|
353
|
+
}))
|
|
354
|
+
throw new Error("The run already sealed its results");
|
|
250
355
|
}
|
|
251
356
|
catch {
|
|
252
357
|
// Paging is advisory after a completed call, including an approved write.
|
|
@@ -271,7 +376,7 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
271
376
|
truncated: true,
|
|
272
377
|
resultId: id,
|
|
273
378
|
totalBytes,
|
|
274
|
-
hint: pagingHint(results, totalBytes, preview),
|
|
379
|
+
hint: pagingHint(results, id, totalBytes, preview),
|
|
275
380
|
nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
|
|
276
381
|
nextAction: results.pagingTool === "read_result"
|
|
277
382
|
? {
|
|
@@ -279,32 +384,37 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
279
384
|
arguments: { id, offset: preview.kind === "prefix" ? preview.bytes : 0 },
|
|
280
385
|
}
|
|
281
386
|
: {
|
|
282
|
-
tool:
|
|
387
|
+
tool: results.routes.program,
|
|
283
388
|
// A byte may cost six JSON characters when escaped. Leave room for the
|
|
284
389
|
// result id, page counters, and continuation metadata in the returned page.
|
|
285
390
|
arguments: {
|
|
286
|
-
code:
|
|
391
|
+
code: results.readBytes !== undefined && totalBytes <= results.readBytes
|
|
392
|
+
? reduceWhole(id, results)
|
|
393
|
+
: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${programPageBytes(results)} })`,
|
|
287
394
|
},
|
|
288
395
|
},
|
|
289
396
|
};
|
|
290
397
|
});
|
|
291
398
|
};
|
|
292
399
|
/** Program calls use the direct-call envelope, ledger, expiry and authority. */
|
|
293
|
-
export function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
|
|
294
|
-
return runEdge(pageProgramResultNative(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes));
|
|
400
|
+
export function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, hostCalls, wireBytes = PROGRAM_RESULT_WIRE_BYTES, routes = META_ROUTES, held, readBytes) {
|
|
401
|
+
return runEdge(pageProgramResultNative(registry, baseUrl, resolved, value, format, secrets, requestScope, hostCalls, wireBytes, routes, held, readBytes));
|
|
295
402
|
}
|
|
296
|
-
const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult, function (registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
|
|
403
|
+
const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult, function (registry, baseUrl, resolved, value, format, secrets, requestScope, hostCalls, wireBytes = PROGRAM_RESULT_WIRE_BYTES, routes = META_ROUTES, held, readBytes) {
|
|
297
404
|
return authFlow(function* () {
|
|
405
|
+
const passed = held?.secrets.revision ?? 0;
|
|
298
406
|
const data = secrets.redact(value);
|
|
299
407
|
const wire = JSON.stringify({ data, format });
|
|
300
408
|
if (enc.encode(wire).length <= wireBytes)
|
|
301
409
|
return { data, format };
|
|
302
|
-
const
|
|
410
|
+
const serialized = format === "text" ? { text: String(data), binary: false } : serializedWithBinary(data);
|
|
411
|
+
const text = secrets.text(serialized.text);
|
|
303
412
|
const bytes = enc.encode(text);
|
|
304
413
|
const write = resolved.definition.classification !== "read";
|
|
414
|
+
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, registry.maxResultBytes);
|
|
305
415
|
const notice = yield* upwardStep(stashResult(bytes, {
|
|
306
|
-
//
|
|
307
|
-
routes
|
|
416
|
+
// Pages name the program route the run is on, listed wherever it runs.
|
|
417
|
+
routes,
|
|
308
418
|
binding: {
|
|
309
419
|
identity: registry.resultIdentity(),
|
|
310
420
|
baseUrl,
|
|
@@ -313,8 +423,9 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
|
|
|
313
423
|
classification: write ? "write" : "read",
|
|
314
424
|
},
|
|
315
425
|
write,
|
|
316
|
-
cap
|
|
317
|
-
|
|
426
|
+
cap,
|
|
427
|
+
...(held ? { held: { results: held, passed, format, binary: serialized.binary } } : {}),
|
|
428
|
+
set: authMethod((id, chunks, ttl, options) => registryProgram(registry, "stashResult", id, chunks, ttl, options)),
|
|
318
429
|
warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
|
|
319
430
|
}, { kind: "none" }, format === "text" ? text : undefined));
|
|
320
431
|
return {
|
|
@@ -324,28 +435,35 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
|
|
|
324
435
|
totalBytes: bytes.length,
|
|
325
436
|
truncated: true,
|
|
326
437
|
hint: notice.resultId
|
|
327
|
-
?
|
|
438
|
+
? readBytes !== undefined && bytes.length <= readBytes
|
|
439
|
+
? `${write ? `${WRITE_ALREADY_RAN} ` : ""}Read it whole with connecta.result(handle, { all: true }), then reduce it.`
|
|
440
|
+
: write
|
|
441
|
+
? `${WRITE_ALREADY_RAN} Page this result with connecta.result(handle); do not repeat the call for output.`
|
|
442
|
+
: readHandleHint(notice.resultId, cap, bytes.length, hostCalls)
|
|
328
443
|
: notice.hint,
|
|
329
444
|
};
|
|
330
445
|
});
|
|
331
446
|
});
|
|
332
447
|
/** Program returns share the direct stash, with all host-admitted call dependencies. */
|
|
333
|
-
export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
|
|
334
|
-
return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars));
|
|
448
|
+
export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars, held, storeBy, readBytes, routes) {
|
|
449
|
+
return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars, held, storeBy, readBytes, routes));
|
|
335
450
|
}
|
|
336
|
-
const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars
|
|
451
|
+
const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars, held,
|
|
452
|
+
// Stalled storage costs the paging, never the answer.
|
|
453
|
+
storeBy, readBytes, routes = META_ROUTES) {
|
|
337
454
|
return authFlow(function* () {
|
|
338
|
-
|
|
339
|
-
if (serialized.length <= maxChars)
|
|
455
|
+
if (serializeResultText(value).length <= maxChars)
|
|
340
456
|
return value;
|
|
341
|
-
const
|
|
457
|
+
const passed = held?.secrets.revision ?? 0;
|
|
458
|
+
const serialized = serializedWithBinary(value);
|
|
459
|
+
const text = secrets.text(serialized.text);
|
|
342
460
|
const bytes = enc.encode(text);
|
|
343
461
|
const write = dependencies.some((dependency) => dependency.classification === "write");
|
|
344
462
|
const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
|
|
345
463
|
const results = {
|
|
346
464
|
secrets,
|
|
347
|
-
// A
|
|
348
|
-
routes
|
|
465
|
+
// A return names the program route the run is on, listed wherever it runs.
|
|
466
|
+
routes,
|
|
349
467
|
binding: {
|
|
350
468
|
identity: registry.resultIdentity(),
|
|
351
469
|
baseUrl,
|
|
@@ -355,7 +473,14 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
|
|
|
355
473
|
write: completedWrite,
|
|
356
474
|
cap,
|
|
357
475
|
...(programChars !== undefined ? { programChars } : {}),
|
|
358
|
-
|
|
476
|
+
...(readBytes !== undefined ? { readBytes } : {}),
|
|
477
|
+
...(held ? { held: { results: held, passed, format: "json", binary: serialized.binary } } : {}),
|
|
478
|
+
set: authMethod((id, chunks, ttl, options) => storeBy === undefined
|
|
479
|
+
? registryProgram(registry, "stashResult", id, chunks, ttl, options)
|
|
480
|
+
: withDeadlineEffect(() => registryProgram(registry, "stashResult", id, chunks, ttl, options), {
|
|
481
|
+
timeoutMs: Math.max(1, storeBy - Date.now()),
|
|
482
|
+
timeoutError: new Error("Result storage did not answer."),
|
|
483
|
+
})),
|
|
359
484
|
warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
|
|
360
485
|
};
|
|
361
486
|
const notice = yield* upwardStep(stashResult(bytes, results, { kind: "none" }, text));
|
|
@@ -380,6 +505,39 @@ function noticeFirst(notice, preview) {
|
|
|
380
505
|
content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
|
|
381
506
|
};
|
|
382
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* The view of a read past its cap, or undefined for its first bytes. A call that
|
|
510
|
+
* may have written is never reshaped, whatever a deployment override calls it: its
|
|
511
|
+
* result is its only copy, and repeating it to look again is forbidden (INV-9).
|
|
512
|
+
*/
|
|
513
|
+
function previewOf(text, bytes, results, format) {
|
|
514
|
+
if (!results.programTool || results.write)
|
|
515
|
+
return undefined;
|
|
516
|
+
// The program this offers has to reach every byte within the run's host calls.
|
|
517
|
+
if (!loopFits(results.cap, bytes.length, results.programHostCalls ?? CONFIG_DEFAULTS.execute.maxHostCalls))
|
|
518
|
+
return undefined;
|
|
519
|
+
return previewResult(text, format, results.cap);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* A preview's notice. It says what the body holds and what it leaves out, and
|
|
523
|
+
* since a host may prompt for or refuse the program, what to do without it.
|
|
524
|
+
*/
|
|
525
|
+
function previewNotice(view, id, totalBytes, tool) {
|
|
526
|
+
return {
|
|
527
|
+
truncated: true,
|
|
528
|
+
resultId: id,
|
|
529
|
+
totalBytes,
|
|
530
|
+
...view.fields,
|
|
531
|
+
hint: `${view.shown} All of it is retained for ${RESULT_TTL_SECONDS / 60} minutes: run nextAction in ${tool}, editing its last line to ${view.noun === "rows" ? "filter, join or count the rows" : "search for what you need"}, instead of reading pages by hand. If ${tool} is refused or unavailable, the rest can't be read here: narrow this read with its own filter, limit or fields arguments, or say what was not read.`,
|
|
532
|
+
nextOffset: 0,
|
|
533
|
+
nextAction: {
|
|
534
|
+
tool,
|
|
535
|
+
arguments: {
|
|
536
|
+
code: `async () => { let text = "", offset = 0, page; do { page = await connecta.result(${JSON.stringify(id)}, { offset }); text += page.text; offset = page.nextOffset; } while (page.hasMore); ${view.reduce} }`,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
383
541
|
/**
|
|
384
542
|
* Return `text` as a single content block; if it exceeds `cap` bytes, stash the
|
|
385
543
|
* full text and return a JSON truncation notice pointing at connecta.result,
|
|
@@ -388,7 +546,7 @@ function noticeFirst(notice, preview) {
|
|
|
388
546
|
* `text` already encoded, so a caller that had to measure it to make this
|
|
389
547
|
* decision doesn't encode it twice.
|
|
390
548
|
*/
|
|
391
|
-
const guardEncoded = function (text, bytes, results, cap) {
|
|
549
|
+
const guardEncoded = function (text, bytes, results, cap, format) {
|
|
392
550
|
return authFlow(function* () {
|
|
393
551
|
if (bytes.length <= cap) {
|
|
394
552
|
return {
|
|
@@ -405,11 +563,17 @@ const guardEncoded = function (text, bytes, results, cap) {
|
|
|
405
563
|
kind: "prefix",
|
|
406
564
|
bytes: head.length,
|
|
407
565
|
}));
|
|
566
|
+
// A preview needs the retained copy its program reads, so a failed stash keeps the first bytes.
|
|
567
|
+
const view = notice.resultId === undefined ? undefined : previewOf(text, bytes, results, format);
|
|
568
|
+
if (view && notice.resultId !== undefined) {
|
|
569
|
+
const shown = previewNotice(view, notice.resultId, bytes.length, results.programTool);
|
|
570
|
+
return { result: noticeFirst(shown, view.body), truncated: true };
|
|
571
|
+
}
|
|
408
572
|
return { result: noticeFirst(notice, dec.decode(head)), truncated: true };
|
|
409
573
|
});
|
|
410
574
|
};
|
|
411
575
|
/** {@link guardEncoded} over a string that has not been measured yet. */
|
|
412
|
-
const guardText = function (text, results, cap) {
|
|
576
|
+
const guardText = function (text, results, cap, format) {
|
|
413
577
|
return authFlow(function* () {
|
|
414
578
|
// `JSON.stringify`'s type says `string` where its behavior says `string |
|
|
415
579
|
// undefined`, so TypeScript alone does not keep a non-string out of here.
|
|
@@ -418,7 +582,7 @@ const guardText = function (text, results, cap) {
|
|
|
418
582
|
// through it the way issue #42 describes.
|
|
419
583
|
const serialized = typeof text === "string" ? text : serializeResultText(text);
|
|
420
584
|
const body = results.secrets?.text(serialized) ?? serialized;
|
|
421
|
-
return yield* upwardStep(guardEncoded(body, enc.encode(body), results, cap));
|
|
585
|
+
return yield* upwardStep(guardEncoded(body, enc.encode(body), results, cap, format));
|
|
422
586
|
});
|
|
423
587
|
};
|
|
424
588
|
/** Store an oversized JSON value and replace it with a page handle. */
|
|
@@ -474,7 +638,7 @@ function escapedHeadOf(bytes, cap) {
|
|
|
474
638
|
* the head of a half-written base64 image is of no use to anyone. Either way
|
|
475
639
|
* the full envelope is stashed and pages through `connecta.result`.
|
|
476
640
|
*/
|
|
477
|
-
const guardContent = function (content, results, cap) {
|
|
641
|
+
const guardContent = function (content, results, cap, format) {
|
|
478
642
|
return authFlow(function* () {
|
|
479
643
|
content = results.secrets?.redact({ content }).content ?? content;
|
|
480
644
|
const only = content.length === 1 ? content[0] : undefined;
|
|
@@ -483,7 +647,7 @@ const guardContent = function (content, results, cap) {
|
|
|
483
647
|
// Under the cap the block passes through untouched, annotations included.
|
|
484
648
|
if (bytes.length <= cap)
|
|
485
649
|
return { result: { content }, truncated: false };
|
|
486
|
-
return yield* upwardStep(guardEncoded(only.text, bytes, results, cap));
|
|
650
|
+
return yield* upwardStep(guardEncoded(only.text, bytes, results, cap, format));
|
|
487
651
|
}
|
|
488
652
|
let text;
|
|
489
653
|
try {
|
|
@@ -540,6 +704,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
540
704
|
// Already normalized and warned about at registry construction.
|
|
541
705
|
const globalCap = registry.maxResultBytes;
|
|
542
706
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
707
|
+
const responseCeilingMs = normalizeTimeoutMs(opts.responseCeilingMs) ?? CONFIG_DEFAULTS.execute.watchdogMs;
|
|
543
708
|
const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
544
709
|
const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
|
|
545
710
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
@@ -558,6 +723,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
558
723
|
}
|
|
559
724
|
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
560
725
|
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
726
|
+
readOnly: opts.readOnly,
|
|
561
727
|
requestScope,
|
|
562
728
|
requestSignal: opts.requestSignal,
|
|
563
729
|
probeTimeoutMs,
|
|
@@ -582,7 +748,22 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
582
748
|
}
|
|
583
749
|
const runCallNative = /* @__PURE__ */ registerAuth(runCall, function (call, source) {
|
|
584
750
|
return authFlow(function* () {
|
|
585
|
-
|
|
751
|
+
// A deadline past the client's would trade a typed result for its transport timeout.
|
|
752
|
+
const now = Date.now();
|
|
753
|
+
const ceilingAt = (opts.requestStartedAt ?? now) + responseCeilingMs;
|
|
754
|
+
const remainingMs = ceilingAt - now;
|
|
755
|
+
const timeoutMs = Math.min(normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs, Math.max(1, remainingMs));
|
|
756
|
+
const pastCeiling = () => ({
|
|
757
|
+
code: "timeout",
|
|
758
|
+
message: `This request used its ${responseCeilingMs}ms response ceiling before ${boundedEchoText(call.address, 512)} started, so it was not sent. Send it again.`,
|
|
759
|
+
retryable: true,
|
|
760
|
+
details: {
|
|
761
|
+
operation: boundedEchoText(call.address, 512),
|
|
762
|
+
stage: "response ceiling",
|
|
763
|
+
elapsedMs: responseCeilingMs - (ceilingAt - Date.now()),
|
|
764
|
+
deadlineMs: responseCeilingMs,
|
|
765
|
+
},
|
|
766
|
+
});
|
|
586
767
|
const outcome = yield* upwardStep(authInvoke(invocation, "invoke", [
|
|
587
768
|
call.address,
|
|
588
769
|
call.args ?? {},
|
|
@@ -592,7 +773,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
592
773
|
sentSecrets,
|
|
593
774
|
readOnly: opts.readOnly,
|
|
594
775
|
routes,
|
|
595
|
-
|
|
776
|
+
timeoutMs,
|
|
777
|
+
// Auth or queueing spent the time, and a 1 ms deadline could still lose
|
|
778
|
+
// to a dispatch. A known "not sent" beats an answer the client won't hear.
|
|
779
|
+
...(remainingMs <= 0 ? { refusal: pastCeiling() } : {}),
|
|
780
|
+
dispatchDeadline: { at: ceilingAt, refusal: pastCeiling },
|
|
596
781
|
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
597
782
|
unwrapResult: call.resultMode === "value",
|
|
598
783
|
...(opts.downstreamInput
|
|
@@ -628,10 +813,12 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
628
813
|
tool: resolved.definition.name,
|
|
629
814
|
classification: resolved.definition.classification === "read" ? "read" : "write",
|
|
630
815
|
},
|
|
631
|
-
write: resolved.definition
|
|
816
|
+
write: isWriteLike(resolved.definition),
|
|
632
817
|
cap,
|
|
633
818
|
...(opts.pagingTool ? { pagingTool: opts.pagingTool } : {}),
|
|
819
|
+
...(opts.programTool ? { programTool: opts.programTool } : {}),
|
|
634
820
|
...(opts.maxProgramResultChars !== undefined ? { programChars: opts.maxProgramResultChars } : {}),
|
|
821
|
+
...(opts.maxProgramHostCalls !== undefined ? { programHostCalls: opts.maxProgramHostCalls } : {}),
|
|
635
822
|
set: authMethod((id, value, ttlSeconds) => registryProgram(registry, "stashResult", id, value, ttlSeconds)),
|
|
636
823
|
// The catalog entry, never `toolName`: a record names a tool
|
|
637
824
|
// only through the grammar check (src/operator-record.ts).
|
|
@@ -660,14 +847,14 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
660
847
|
},
|
|
661
848
|
];
|
|
662
849
|
}
|
|
663
|
-
const guarded = yield* upwardStep(guardContent(content, results, cap));
|
|
850
|
+
const guarded = yield* upwardStep(guardContent(content, results, cap, format));
|
|
664
851
|
// Native blocks and bounded previews are the result. A format-only
|
|
665
852
|
// structuredContent hides them in clients that prefer that field.
|
|
666
853
|
guarded.result._meta = { "dev.connecta/format": format };
|
|
667
854
|
return processed(guarded.result, guarded.truncated);
|
|
668
855
|
}
|
|
669
856
|
const value = result;
|
|
670
|
-
const guarded = yield* upwardStep(guardText(serializeResultText(value), results, cap));
|
|
857
|
+
const guarded = yield* upwardStep(guardText(serializeResultText(value), results, cap, format));
|
|
671
858
|
guarded.result._meta = { "dev.connecta/format": format };
|
|
672
859
|
return processed(guarded.result, guarded.truncated, { value });
|
|
673
860
|
})),
|
|
@@ -696,10 +883,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
696
883
|
// both copies and JSON escaping when the bounded provider reason is large.
|
|
697
884
|
if (!recoveryRequired) {
|
|
698
885
|
const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
|
|
699
|
-
|
|
886
|
+
const { message } = outcome.error;
|
|
887
|
+
let budget = 2 * MAX_TOOL_ERROR_BYTES;
|
|
700
888
|
while (enc.encode(JSON.stringify(failedResult)).length > cap && budget > 0) {
|
|
701
889
|
budget = Math.floor(budget / 2);
|
|
702
|
-
outcome.error.message =
|
|
890
|
+
outcome.error.message = clipText(message, budget);
|
|
703
891
|
failedResult = makeFailedResult();
|
|
704
892
|
}
|
|
705
893
|
}
|
|
@@ -731,7 +919,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
731
919
|
};
|
|
732
920
|
});
|
|
733
921
|
});
|
|
734
|
-
|
|
922
|
+
const methods = {
|
|
735
923
|
skills: authMethod((args = {}) => authFlow(function* () {
|
|
736
924
|
try {
|
|
737
925
|
if (!args.name) {
|
|
@@ -748,10 +936,13 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
748
936
|
};
|
|
749
937
|
}
|
|
750
938
|
const text = yield* upwardStep(authInvoke(skills, "text", [args.name]));
|
|
751
|
-
|
|
939
|
+
// Guidance is text-only: a structured copy would double every fetch on the wire (result-representation).
|
|
940
|
+
return { content: [{ type: "text", text }] };
|
|
752
941
|
}
|
|
753
942
|
catch (error) {
|
|
754
|
-
|
|
943
|
+
if (error instanceof SkillLookupError)
|
|
944
|
+
return toolError(skillLookupRecovery(error.message));
|
|
945
|
+
return toolError(utilityRecovery(error instanceof ConnectorCallError ? error.code : "unavailable", "Skills are unavailable.", error instanceof ConnectorCallError && error.retryable));
|
|
755
946
|
}
|
|
756
947
|
})),
|
|
757
948
|
searchTools: authMethod((args) => authFlow(function* () {
|
|
@@ -781,25 +972,21 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
781
972
|
// Defense for in-process callers too: guest paging validates these domains
|
|
782
973
|
// before calling here, while internal consumers may call readResult directly.
|
|
783
974
|
if (args.maxBytes !== undefined && !isValidMaxResultBytes(args.maxBytes)) {
|
|
784
|
-
return
|
|
975
|
+
return resultError(`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
|
|
785
976
|
`>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`);
|
|
786
977
|
}
|
|
787
978
|
if (args.offset !== undefined && !isValidResultOffset(args.offset)) {
|
|
788
|
-
return
|
|
979
|
+
return resultError(`Invalid offset ${args.offset}: must be a whole number of bytes ` +
|
|
789
980
|
`>= ${MIN_RESULT_OFFSET}. Omit it to start at the beginning.`);
|
|
790
981
|
}
|
|
791
982
|
if (args.page !== undefined &&
|
|
792
983
|
(!Number.isSafeInteger(args.page) || args.page < 0 || args.offset !== undefined)) {
|
|
793
|
-
return
|
|
984
|
+
return resultError("Use a whole page >= 0, without offset.");
|
|
794
985
|
}
|
|
795
986
|
const results = registry.resultsStorage();
|
|
796
987
|
const unavailableResult = () => ({
|
|
797
988
|
...jsonResult({
|
|
798
|
-
error:
|
|
799
|
-
code: "unavailable",
|
|
800
|
-
message: "Result paging storage is unavailable.",
|
|
801
|
-
retryable: true,
|
|
802
|
-
},
|
|
989
|
+
error: utilityRecovery("unavailable", "Result paging storage is unavailable.", true),
|
|
803
990
|
}),
|
|
804
991
|
isError: true,
|
|
805
992
|
});
|
|
@@ -821,14 +1008,16 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
821
1008
|
options.signal?.throwIfAborted();
|
|
822
1009
|
return value;
|
|
823
1010
|
}));
|
|
824
|
-
const stored =
|
|
1011
|
+
const stored = opts.heldResults?.stored(args.id) ??
|
|
1012
|
+
(yield* upwardStep(authCall(read, [resultKeys.chunk(args.id, 0)])));
|
|
825
1013
|
if (stored === false)
|
|
826
1014
|
return unavailableResult();
|
|
827
1015
|
if (stored === null) {
|
|
828
|
-
return
|
|
1016
|
+
return toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
|
|
829
1017
|
}
|
|
830
|
-
const denied = () =>
|
|
831
|
-
const
|
|
1018
|
+
const denied = () => toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
|
|
1019
|
+
const refused = RESULT_REFUSED_V4_HEADER.exec(stored);
|
|
1020
|
+
const header = refused ?? RESULT_ENVELOPE_V4_HEADER.exec(stored);
|
|
832
1021
|
if (!header)
|
|
833
1022
|
return denied();
|
|
834
1023
|
let binding;
|
|
@@ -858,6 +1047,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
858
1047
|
!["read", "write"].includes(dependency.classification))) ||
|
|
859
1048
|
!["read", "write"].includes(binding.classification))
|
|
860
1049
|
return denied();
|
|
1050
|
+
if (refused)
|
|
1051
|
+
return toolError(resultNotRetained(binding.classification === "write", args.id));
|
|
861
1052
|
const allowed = authMethod(() => authFlow(function* () {
|
|
862
1053
|
options.onStage?.("access validation");
|
|
863
1054
|
options.signal?.throwIfAborted();
|
|
@@ -941,7 +1132,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
941
1132
|
if (encoded === null) {
|
|
942
1133
|
// A chunk expired or was evicted under its own header; the id can no
|
|
943
1134
|
// longer serve this range, and inventing U+0000 filler would be worse.
|
|
944
|
-
return
|
|
1135
|
+
return toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
|
|
945
1136
|
}
|
|
946
1137
|
const chunkStart = index * chunkBytes;
|
|
947
1138
|
// Both bounds are chunk-local. `from` inherits `start`'s three-byte
|
|
@@ -999,18 +1190,27 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
999
1190
|
}
|
|
1000
1191
|
})),
|
|
1001
1192
|
};
|
|
1193
|
+
if (!opts.activity?.recordOperation)
|
|
1194
|
+
return methods;
|
|
1195
|
+
const track = (name, method) => authMethod((...args) => operationActivity(opts.activity, registry, name, authCall(method, args)));
|
|
1196
|
+
return {
|
|
1197
|
+
...methods,
|
|
1198
|
+
skills: track("skill", methods.skills),
|
|
1199
|
+
searchTools: track("search", methods.searchTools),
|
|
1200
|
+
readResult: track("result", methods.readResult),
|
|
1201
|
+
};
|
|
1002
1202
|
}
|
|
1003
1203
|
// Each description states only its own tool's scope; the full routing rule
|
|
1004
1204
|
// lives once in the server instructions. A client that drops instructions
|
|
1005
1205
|
// still routes correctly from these scopes plus execute_code's terse lead.
|
|
1006
1206
|
// Only the meta surface lists these tools, so they use its names.
|
|
1007
|
-
const SEARCH_DESC = (readOnly) => `${readOnly ? "
|
|
1008
|
-
const CALL_DESC = (readOnly) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${
|
|
1009
|
-
const CALL_DESTRUCTIVE_DESC = "Call
|
|
1207
|
+
const SEARCH_DESC = (readOnly, program) => `${readOnly ? "Inspect the catalog; this read-only endpoint omits write tools." : `Inspect the catalog, or find a write before ${META_ROUTES.write}.`} For read work, search inside one ${program} program that also calls and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; set connector when known; an empty query browses. safety filters by classification and grants nothing. Read catalogErrors and absence before choosing a tool.`;
|
|
1208
|
+
const CALL_DESC = (readOnly, program) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${program}. ${readOnly ? "This endpoint is read-only and refuses every write." : `Unannotated or write-capable tools fail closed to ${META_ROUTES.write}.`} A truncated result carries nextAction.`;
|
|
1209
|
+
const CALL_DESTRUCTIVE_DESC = "Call one tool not explicitly annotated readOnlyHint: true. Check its schema and consequences, then include a short reason for the human reviewer; it grants nothing and is not sent downstream. Sent once: never resend it to see output.";
|
|
1010
1210
|
/** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
|
|
1011
1211
|
const CALL_DESTRUCTIVE_READ_ONLY_DESC = `This endpoint is read-only: search and describe omit write tools, and every write call here is refused. Reads still run; prefer ${META_ROUTES.read} or ${META_ROUTES.program} for them.`;
|
|
1012
|
-
const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or required deployment configuration. A configured credential reports presence only: if a call just failed
|
|
1013
|
-
const SKILLS_DESC = "List or fetch
|
|
1212
|
+
const AUTHORIZE_DESC = "Use after auth_required when the error has no authorizationUrl or operatorUrl, or for a fresh link; otherwise pass that on. Returns an OAuth or operator-credential handoff, or required deployment configuration. A configured credential reports presence only: if a call just failed on authentication or configuration, give the retained handoff to the operator; otherwise proceed with the call. force=true restarts OAuth when the verified user opens the /connect URL. Never accepts credentials.";
|
|
1213
|
+
const SKILLS_DESC = "List or fetch guidance by exact name. Fetch usage only when guidance is missing or a program needs repair.";
|
|
1014
1214
|
/**
|
|
1015
1215
|
* Sentences appended to a meta-tool description only when this connection
|
|
1016
1216
|
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
@@ -1020,9 +1220,9 @@ const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the
|
|
|
1020
1220
|
* Registration is per connection and reads the configured connector set.
|
|
1021
1221
|
*/
|
|
1022
1222
|
const GUIDE_NOTES = {
|
|
1023
|
-
skills: "
|
|
1024
|
-
search: "
|
|
1025
|
-
destructive: " Fetch any
|
|
1223
|
+
skills: " Includes connector guides.",
|
|
1224
|
+
search: " guideRequired: true means fetch its named guide before the call.",
|
|
1225
|
+
destructive: " Fetch any connector guide discovery names first.",
|
|
1026
1226
|
};
|
|
1027
1227
|
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
1028
1228
|
function describedFor(registry, base, note) {
|
|
@@ -1047,7 +1247,10 @@ const READ_ONLY_LOCAL = {
|
|
|
1047
1247
|
openWorldHint: false,
|
|
1048
1248
|
};
|
|
1049
1249
|
const CALL_INPUT_SCHEMA = {
|
|
1050
|
-
|
|
1250
|
+
// Error text only: neither message reaches the advertised schema or widens what is accepted.
|
|
1251
|
+
address: z
|
|
1252
|
+
.string({ error: 'expected a string such as "connector.tool". Send { address, args }.' })
|
|
1253
|
+
.meta({ "x-mcp-header": "Address" }),
|
|
1051
1254
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
1052
1255
|
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1053
1256
|
timeoutMs: z.number().int().positive().optional(),
|
|
@@ -1067,7 +1270,13 @@ const SEARCH_INPUT = advertisedSchema(z.object({
|
|
|
1067
1270
|
fullDescriptions: z.boolean().optional(),
|
|
1068
1271
|
includeSchemas: z.enum(["compact", "json", "typescript"]).optional(),
|
|
1069
1272
|
}));
|
|
1070
|
-
|
|
1273
|
+
/** MCP's own `tools/call` spells these `name` and `arguments`; say so at the point of failure. */
|
|
1274
|
+
const CALL_SHAPE = {
|
|
1275
|
+
error: (issue) => issue.code === "unrecognized_keys"
|
|
1276
|
+
? `Unrecognized key${issue.keys?.length === 1 ? "" : "s"}: ${issue.keys?.map((key) => JSON.stringify(key)).join(", ")}. Send { address, args }, with the tool's arguments in args.`
|
|
1277
|
+
: undefined,
|
|
1278
|
+
};
|
|
1279
|
+
const CALL_INPUT = advertisedSchema(z.strictObject(CALL_INPUT_SCHEMA, CALL_SHAPE));
|
|
1071
1280
|
const CALL_DESTRUCTIVE_INPUT = advertisedSchema(z.strictObject({
|
|
1072
1281
|
...CALL_INPUT_SCHEMA,
|
|
1073
1282
|
// Bounded above, but with no lower bound: a model that sends `""` or
|
|
@@ -1075,8 +1284,8 @@ const CALL_DESTRUCTIVE_INPUT = advertisedSchema(z.strictObject({
|
|
|
1075
1284
|
// call over a cosmetic field the host merely displays is the wrong
|
|
1076
1285
|
// trade. It is normalized to absent in the handler instead.
|
|
1077
1286
|
reason: z.string().max(500).optional(),
|
|
1078
|
-
}));
|
|
1079
|
-
const READ_RESULT_DESC = "Page
|
|
1287
|
+
}, CALL_SHAPE));
|
|
1288
|
+
const READ_RESULT_DESC = "Page a truncated result: run nextAction, then continue from nextOffset while hasMore. Never repeat a write for output.";
|
|
1080
1289
|
const READ_RESULT_INPUT = advertisedSchema(z.strictObject({
|
|
1081
1290
|
id: z.string(),
|
|
1082
1291
|
offset: z.number().int().nonnegative().optional(),
|
|
@@ -1098,6 +1307,8 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
|
|
|
1098
1307
|
export function registerMetaTools(server, registry, ctx) {
|
|
1099
1308
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1100
1309
|
maxProgramResultChars: ctx.maxProgramResultChars,
|
|
1310
|
+
maxProgramHostCalls: ctx.maxProgramHostCalls,
|
|
1311
|
+
programTool: ctx.programTool,
|
|
1101
1312
|
routes: ctx.routes,
|
|
1102
1313
|
...(ctx.native && !ctx.native.executeCode ? { pagingTool: "read_result" } : {}),
|
|
1103
1314
|
...(ctx.native ? { activitySource: "native" } : {}),
|
|
@@ -1105,6 +1316,8 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1105
1316
|
requestScope: ctx.requestScope,
|
|
1106
1317
|
readOnly: ctx.readOnly,
|
|
1107
1318
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1319
|
+
responseCeilingMs: ctx.responseCeilingMs,
|
|
1320
|
+
requestStartedAt: ctx.requestStartedAt,
|
|
1108
1321
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1109
1322
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
1110
1323
|
activity: ctx.activity,
|
|
@@ -1116,6 +1329,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1116
1329
|
requestSignal: ctx.requestSignal,
|
|
1117
1330
|
defer: ctx.defer,
|
|
1118
1331
|
});
|
|
1332
|
+
const routeProgram = ctx.routes?.program ?? META_ROUTES.program;
|
|
1119
1333
|
const bindRequest = (request) => {
|
|
1120
1334
|
if (ctx.client)
|
|
1121
1335
|
bindMcpClient(request, ctx.client, ctx.activity);
|
|
@@ -1124,7 +1338,6 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1124
1338
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1125
1339
|
...(ctx.native ? { _meta: { "dev.connecta/surface": { surface: "native", ...ctx.native } } } : {}),
|
|
1126
1340
|
inputSchema: SKILLS_INPUT,
|
|
1127
|
-
outputSchema: SKILLS_OUTPUT,
|
|
1128
1341
|
annotations: READ_ONLY_LOCAL,
|
|
1129
1342
|
}, authMethod((args, request) => authFlow(function* () {
|
|
1130
1343
|
bindRequest(request);
|
|
@@ -1179,7 +1392,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1179
1392
|
});
|
|
1180
1393
|
}
|
|
1181
1394
|
server.registerTool("search_tools", {
|
|
1182
|
-
description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true), "search"),
|
|
1395
|
+
description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true, routeProgram), "search"),
|
|
1183
1396
|
inputSchema: SEARCH_INPUT,
|
|
1184
1397
|
outputSchema: SEARCH_OUTPUT,
|
|
1185
1398
|
annotations: READ_ONLY_REMOTE,
|
|
@@ -1188,7 +1401,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1188
1401
|
return yield* upwardStep(authInvoke(mt, "searchTools", [args]));
|
|
1189
1402
|
})));
|
|
1190
1403
|
server.registerTool("call_tool", {
|
|
1191
|
-
description: CALL_DESC(ctx.readOnly === true),
|
|
1404
|
+
description: CALL_DESC(ctx.readOnly === true, routeProgram),
|
|
1192
1405
|
inputSchema: CALL_INPUT,
|
|
1193
1406
|
// Default mode returns native content, so no structured result is promised.
|
|
1194
1407
|
// call_tool admits only tools that are themselves explicitly read-only;
|