@zackbart/connecta 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -0
- package/dist/invocation.js +98 -34
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +1 -1
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/meta-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { operationActivity } from "./operation-activity.js";
|
|
2
3
|
import { SkillLookupError } from "./skills.js";
|
|
3
4
|
import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
|
|
@@ -6,26 +7,29 @@ import { toolError } from "./tool-error.js";
|
|
|
6
7
|
import { upwardStep } from "./upward.js";
|
|
7
8
|
import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
|
|
8
9
|
import { registryProgram } from "./runtime/registry.js";
|
|
9
|
-
import { runEdge } from "./runtime/run.js";
|
|
10
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
10
11
|
import { authorizationHandoff, authorizeConnector, bindAuthorizationHandoffs } from "./authorization-handoff.js";
|
|
11
12
|
import { bindMcpClient } from "./mcp-client-context.js";
|
|
12
13
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
13
14
|
import * as z from "zod";
|
|
14
15
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
15
|
-
import { boundedDiscoveryText, CatalogService,
|
|
16
|
+
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
16
17
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
17
|
-
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
18
|
+
import { boundedEchoText, clipText, ConnectorCallError, MAX_TOOL_ERROR_BYTES, msg, } from "./errors.js";
|
|
18
19
|
import { failureRecord, logFailure } from "./operator-record.js";
|
|
20
|
+
import { RESULT_ENVELOPE_V4 } from "./result-seal.js";
|
|
19
21
|
import { guardExecuteResultValue, MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
|
|
20
22
|
import { InvocationService } from "./invocation.js";
|
|
21
23
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
22
24
|
import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
|
|
23
25
|
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
|
|
24
|
-
import {
|
|
26
|
+
import { isWriteLike } from "./tool-safety.js";
|
|
27
|
+
import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT } from "./meta-output.js";
|
|
25
28
|
import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
|
|
26
29
|
import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
|
|
27
30
|
import { captureDownstreamInput } from "./downstream-input.js";
|
|
28
31
|
import { PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
|
|
32
|
+
import { previewResult } from "./result-preview.js";
|
|
29
33
|
export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
|
|
30
34
|
const enc = new TextEncoder();
|
|
31
35
|
// Each page is a slice of payload bytes, so a leading U+FEFF is data.
|
|
@@ -116,8 +120,8 @@ function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
116
120
|
return e;
|
|
117
121
|
}
|
|
118
122
|
/** Every readable stash carries host-admitted bindings; older entries fail closed. */
|
|
119
|
-
const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
|
|
120
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+/=]+):$/;
|
|
121
125
|
/**
|
|
122
126
|
* Smallest chunk of result text stored under one key. A multiple of three so
|
|
123
127
|
* every chunk's base64 stands alone and a byte offset inside it lands on a
|
|
@@ -137,6 +141,30 @@ const RESULT_MAX_CHUNKS = 32;
|
|
|
137
141
|
function resultChunkBytes(totalBytes) {
|
|
138
142
|
return Math.max(RESULT_CHUNK_BYTES, Math.ceil(totalBytes / RESULT_MAX_CHUNKS / 3) * 3);
|
|
139
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
|
+
}
|
|
140
168
|
/**
|
|
141
169
|
* Base64 of `bytes`, in three-byte-aligned batches so the argument list of one
|
|
142
170
|
* spread never grows with the result. Alignment matters: an unaligned batch
|
|
@@ -149,6 +177,26 @@ function base64Of(bytes) {
|
|
|
149
177
|
}
|
|
150
178
|
return out;
|
|
151
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
|
+
}
|
|
152
200
|
/**
|
|
153
201
|
* The page a notice's nextAction asks a program to read: it fits the program's
|
|
154
202
|
* return even if every byte escapes to six JSON characters, with room for the
|
|
@@ -158,6 +206,28 @@ function programPageBytes(results) {
|
|
|
158
206
|
const fits = Math.floor(((results.programChars ?? MAX_EXECUTE_RESULT_CHARS) - 1024) / 6);
|
|
159
207
|
return Math.max(4, Math.min(fits, results.cap, PROGRAM_RESULT_PAGE_BYTES));
|
|
160
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)}); }`;
|
|
161
231
|
/** Opens a write's notice, ahead of any paging instruction. */
|
|
162
232
|
const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
|
|
163
233
|
/** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
|
|
@@ -186,7 +256,9 @@ function isWellFormedText(text) {
|
|
|
186
256
|
* it is the one thing its notice forbids.
|
|
187
257
|
*/
|
|
188
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`;
|
|
189
|
-
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.`;
|
|
190
262
|
if (results.pagingTool === "read_result") {
|
|
191
263
|
const shown = preview.kind === "prefix"
|
|
192
264
|
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest`
|
|
@@ -203,6 +275,11 @@ function pagingHint(results, totalBytes, preview) {
|
|
|
203
275
|
: preview.kind === "text-of-envelope"
|
|
204
276
|
? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
|
|
205
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.`;
|
|
206
283
|
return `${shown} ${readReduceFirst(results.routes)}; ${full} with connecta.result using nextAction.`;
|
|
207
284
|
}
|
|
208
285
|
const body = preview.kind === "prefix"
|
|
@@ -242,7 +319,8 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
242
319
|
// reader below maps a byte offset back to chunk index and base64 quad.
|
|
243
320
|
const chunkBytes = resultChunkBytes(totalBytes);
|
|
244
321
|
const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
|
|
245
|
-
const
|
|
322
|
+
const head = `${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`;
|
|
323
|
+
const chunks = [head];
|
|
246
324
|
for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
|
|
247
325
|
const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
|
|
248
326
|
if (offset === 0)
|
|
@@ -250,9 +328,30 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
250
328
|
else
|
|
251
329
|
chunks.push(chunk);
|
|
252
330
|
}
|
|
253
|
-
|
|
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)
|
|
254
338
|
throw new Error("Result stash capacity exhausted");
|
|
255
|
-
|
|
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");
|
|
256
355
|
}
|
|
257
356
|
catch {
|
|
258
357
|
// Paging is advisory after a completed call, including an approved write.
|
|
@@ -277,7 +376,7 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
277
376
|
truncated: true,
|
|
278
377
|
resultId: id,
|
|
279
378
|
totalBytes,
|
|
280
|
-
hint: pagingHint(results, totalBytes, preview),
|
|
379
|
+
hint: pagingHint(results, id, totalBytes, preview),
|
|
281
380
|
nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
|
|
282
381
|
nextAction: results.pagingTool === "read_result"
|
|
283
382
|
? {
|
|
@@ -285,32 +384,37 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
285
384
|
arguments: { id, offset: preview.kind === "prefix" ? preview.bytes : 0 },
|
|
286
385
|
}
|
|
287
386
|
: {
|
|
288
|
-
tool:
|
|
387
|
+
tool: results.routes.program,
|
|
289
388
|
// A byte may cost six JSON characters when escaped. Leave room for the
|
|
290
389
|
// result id, page counters, and continuation metadata in the returned page.
|
|
291
390
|
arguments: {
|
|
292
|
-
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)} })`,
|
|
293
394
|
},
|
|
294
395
|
},
|
|
295
396
|
};
|
|
296
397
|
});
|
|
297
398
|
};
|
|
298
399
|
/** Program calls use the direct-call envelope, ledger, expiry and authority. */
|
|
299
|
-
export function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
|
|
300
|
-
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));
|
|
301
402
|
}
|
|
302
|
-
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) {
|
|
303
404
|
return authFlow(function* () {
|
|
405
|
+
const passed = held?.secrets.revision ?? 0;
|
|
304
406
|
const data = secrets.redact(value);
|
|
305
407
|
const wire = JSON.stringify({ data, format });
|
|
306
408
|
if (enc.encode(wire).length <= wireBytes)
|
|
307
409
|
return { data, format };
|
|
308
|
-
const
|
|
410
|
+
const serialized = format === "text" ? { text: String(data), binary: false } : serializedWithBinary(data);
|
|
411
|
+
const text = secrets.text(serialized.text);
|
|
309
412
|
const bytes = enc.encode(text);
|
|
310
413
|
const write = resolved.definition.classification !== "read";
|
|
414
|
+
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, registry.maxResultBytes);
|
|
311
415
|
const notice = yield* upwardStep(stashResult(bytes, {
|
|
312
|
-
//
|
|
313
|
-
routes
|
|
416
|
+
// Pages name the program route the run is on, listed wherever it runs.
|
|
417
|
+
routes,
|
|
314
418
|
binding: {
|
|
315
419
|
identity: registry.resultIdentity(),
|
|
316
420
|
baseUrl,
|
|
@@ -319,8 +423,9 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
|
|
|
319
423
|
classification: write ? "write" : "read",
|
|
320
424
|
},
|
|
321
425
|
write,
|
|
322
|
-
cap
|
|
323
|
-
|
|
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)),
|
|
324
429
|
warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
|
|
325
430
|
}, { kind: "none" }, format === "text" ? text : undefined));
|
|
326
431
|
return {
|
|
@@ -330,28 +435,35 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
|
|
|
330
435
|
totalBytes: bytes.length,
|
|
331
436
|
truncated: true,
|
|
332
437
|
hint: notice.resultId
|
|
333
|
-
?
|
|
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)
|
|
334
443
|
: notice.hint,
|
|
335
444
|
};
|
|
336
445
|
});
|
|
337
446
|
});
|
|
338
447
|
/** Program returns share the direct stash, with all host-admitted call dependencies. */
|
|
339
|
-
export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
|
|
340
|
-
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));
|
|
341
450
|
}
|
|
342
|
-
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) {
|
|
343
454
|
return authFlow(function* () {
|
|
344
|
-
|
|
345
|
-
if (serialized.length <= maxChars)
|
|
455
|
+
if (serializeResultText(value).length <= maxChars)
|
|
346
456
|
return value;
|
|
347
|
-
const
|
|
457
|
+
const passed = held?.secrets.revision ?? 0;
|
|
458
|
+
const serialized = serializedWithBinary(value);
|
|
459
|
+
const text = secrets.text(serialized.text);
|
|
348
460
|
const bytes = enc.encode(text);
|
|
349
461
|
const write = dependencies.some((dependency) => dependency.classification === "write");
|
|
350
462
|
const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
|
|
351
463
|
const results = {
|
|
352
464
|
secrets,
|
|
353
|
-
// A
|
|
354
|
-
routes
|
|
465
|
+
// A return names the program route the run is on, listed wherever it runs.
|
|
466
|
+
routes,
|
|
355
467
|
binding: {
|
|
356
468
|
identity: registry.resultIdentity(),
|
|
357
469
|
baseUrl,
|
|
@@ -361,7 +473,14 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
|
|
|
361
473
|
write: completedWrite,
|
|
362
474
|
cap,
|
|
363
475
|
...(programChars !== undefined ? { programChars } : {}),
|
|
364
|
-
|
|
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
|
+
})),
|
|
365
484
|
warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
|
|
366
485
|
};
|
|
367
486
|
const notice = yield* upwardStep(stashResult(bytes, results, { kind: "none" }, text));
|
|
@@ -386,6 +505,39 @@ function noticeFirst(notice, preview) {
|
|
|
386
505
|
content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
|
|
387
506
|
};
|
|
388
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
|
+
}
|
|
389
541
|
/**
|
|
390
542
|
* Return `text` as a single content block; if it exceeds `cap` bytes, stash the
|
|
391
543
|
* full text and return a JSON truncation notice pointing at connecta.result,
|
|
@@ -394,7 +546,7 @@ function noticeFirst(notice, preview) {
|
|
|
394
546
|
* `text` already encoded, so a caller that had to measure it to make this
|
|
395
547
|
* decision doesn't encode it twice.
|
|
396
548
|
*/
|
|
397
|
-
const guardEncoded = function (text, bytes, results, cap) {
|
|
549
|
+
const guardEncoded = function (text, bytes, results, cap, format) {
|
|
398
550
|
return authFlow(function* () {
|
|
399
551
|
if (bytes.length <= cap) {
|
|
400
552
|
return {
|
|
@@ -411,11 +563,17 @@ const guardEncoded = function (text, bytes, results, cap) {
|
|
|
411
563
|
kind: "prefix",
|
|
412
564
|
bytes: head.length,
|
|
413
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
|
+
}
|
|
414
572
|
return { result: noticeFirst(notice, dec.decode(head)), truncated: true };
|
|
415
573
|
});
|
|
416
574
|
};
|
|
417
575
|
/** {@link guardEncoded} over a string that has not been measured yet. */
|
|
418
|
-
const guardText = function (text, results, cap) {
|
|
576
|
+
const guardText = function (text, results, cap, format) {
|
|
419
577
|
return authFlow(function* () {
|
|
420
578
|
// `JSON.stringify`'s type says `string` where its behavior says `string |
|
|
421
579
|
// undefined`, so TypeScript alone does not keep a non-string out of here.
|
|
@@ -424,7 +582,7 @@ const guardText = function (text, results, cap) {
|
|
|
424
582
|
// through it the way issue #42 describes.
|
|
425
583
|
const serialized = typeof text === "string" ? text : serializeResultText(text);
|
|
426
584
|
const body = results.secrets?.text(serialized) ?? serialized;
|
|
427
|
-
return yield* upwardStep(guardEncoded(body, enc.encode(body), results, cap));
|
|
585
|
+
return yield* upwardStep(guardEncoded(body, enc.encode(body), results, cap, format));
|
|
428
586
|
});
|
|
429
587
|
};
|
|
430
588
|
/** Store an oversized JSON value and replace it with a page handle. */
|
|
@@ -480,7 +638,7 @@ function escapedHeadOf(bytes, cap) {
|
|
|
480
638
|
* the head of a half-written base64 image is of no use to anyone. Either way
|
|
481
639
|
* the full envelope is stashed and pages through `connecta.result`.
|
|
482
640
|
*/
|
|
483
|
-
const guardContent = function (content, results, cap) {
|
|
641
|
+
const guardContent = function (content, results, cap, format) {
|
|
484
642
|
return authFlow(function* () {
|
|
485
643
|
content = results.secrets?.redact({ content }).content ?? content;
|
|
486
644
|
const only = content.length === 1 ? content[0] : undefined;
|
|
@@ -489,7 +647,7 @@ const guardContent = function (content, results, cap) {
|
|
|
489
647
|
// Under the cap the block passes through untouched, annotations included.
|
|
490
648
|
if (bytes.length <= cap)
|
|
491
649
|
return { result: { content }, truncated: false };
|
|
492
|
-
return yield* upwardStep(guardEncoded(only.text, bytes, results, cap));
|
|
650
|
+
return yield* upwardStep(guardEncoded(only.text, bytes, results, cap, format));
|
|
493
651
|
}
|
|
494
652
|
let text;
|
|
495
653
|
try {
|
|
@@ -546,6 +704,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
546
704
|
// Already normalized and warned about at registry construction.
|
|
547
705
|
const globalCap = registry.maxResultBytes;
|
|
548
706
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
707
|
+
const responseCeilingMs = normalizeTimeoutMs(opts.responseCeilingMs) ?? CONFIG_DEFAULTS.execute.watchdogMs;
|
|
549
708
|
const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
550
709
|
const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
|
|
551
710
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
@@ -589,7 +748,22 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
589
748
|
}
|
|
590
749
|
const runCallNative = /* @__PURE__ */ registerAuth(runCall, function (call, source) {
|
|
591
750
|
return authFlow(function* () {
|
|
592
|
-
|
|
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
|
+
});
|
|
593
767
|
const outcome = yield* upwardStep(authInvoke(invocation, "invoke", [
|
|
594
768
|
call.address,
|
|
595
769
|
call.args ?? {},
|
|
@@ -599,7 +773,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
599
773
|
sentSecrets,
|
|
600
774
|
readOnly: opts.readOnly,
|
|
601
775
|
routes,
|
|
602
|
-
|
|
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 },
|
|
603
781
|
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
604
782
|
unwrapResult: call.resultMode === "value",
|
|
605
783
|
...(opts.downstreamInput
|
|
@@ -635,10 +813,12 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
635
813
|
tool: resolved.definition.name,
|
|
636
814
|
classification: resolved.definition.classification === "read" ? "read" : "write",
|
|
637
815
|
},
|
|
638
|
-
write: resolved.definition
|
|
816
|
+
write: isWriteLike(resolved.definition),
|
|
639
817
|
cap,
|
|
640
818
|
...(opts.pagingTool ? { pagingTool: opts.pagingTool } : {}),
|
|
819
|
+
...(opts.programTool ? { programTool: opts.programTool } : {}),
|
|
641
820
|
...(opts.maxProgramResultChars !== undefined ? { programChars: opts.maxProgramResultChars } : {}),
|
|
821
|
+
...(opts.maxProgramHostCalls !== undefined ? { programHostCalls: opts.maxProgramHostCalls } : {}),
|
|
642
822
|
set: authMethod((id, value, ttlSeconds) => registryProgram(registry, "stashResult", id, value, ttlSeconds)),
|
|
643
823
|
// The catalog entry, never `toolName`: a record names a tool
|
|
644
824
|
// only through the grammar check (src/operator-record.ts).
|
|
@@ -667,14 +847,14 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
667
847
|
},
|
|
668
848
|
];
|
|
669
849
|
}
|
|
670
|
-
const guarded = yield* upwardStep(guardContent(content, results, cap));
|
|
850
|
+
const guarded = yield* upwardStep(guardContent(content, results, cap, format));
|
|
671
851
|
// Native blocks and bounded previews are the result. A format-only
|
|
672
852
|
// structuredContent hides them in clients that prefer that field.
|
|
673
853
|
guarded.result._meta = { "dev.connecta/format": format };
|
|
674
854
|
return processed(guarded.result, guarded.truncated);
|
|
675
855
|
}
|
|
676
856
|
const value = result;
|
|
677
|
-
const guarded = yield* upwardStep(guardText(serializeResultText(value), results, cap));
|
|
857
|
+
const guarded = yield* upwardStep(guardText(serializeResultText(value), results, cap, format));
|
|
678
858
|
guarded.result._meta = { "dev.connecta/format": format };
|
|
679
859
|
return processed(guarded.result, guarded.truncated, { value });
|
|
680
860
|
})),
|
|
@@ -703,10 +883,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
703
883
|
// both copies and JSON escaping when the bounded provider reason is large.
|
|
704
884
|
if (!recoveryRequired) {
|
|
705
885
|
const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
|
|
706
|
-
|
|
886
|
+
const { message } = outcome.error;
|
|
887
|
+
let budget = 2 * MAX_TOOL_ERROR_BYTES;
|
|
707
888
|
while (enc.encode(JSON.stringify(failedResult)).length > cap && budget > 0) {
|
|
708
889
|
budget = Math.floor(budget / 2);
|
|
709
|
-
outcome.error.message =
|
|
890
|
+
outcome.error.message = clipText(message, budget);
|
|
710
891
|
failedResult = makeFailedResult();
|
|
711
892
|
}
|
|
712
893
|
}
|
|
@@ -755,7 +936,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
755
936
|
};
|
|
756
937
|
}
|
|
757
938
|
const text = yield* upwardStep(authInvoke(skills, "text", [args.name]));
|
|
758
|
-
|
|
939
|
+
// Guidance is text-only: a structured copy would double every fetch on the wire (result-representation).
|
|
940
|
+
return { content: [{ type: "text", text }] };
|
|
759
941
|
}
|
|
760
942
|
catch (error) {
|
|
761
943
|
if (error instanceof SkillLookupError)
|
|
@@ -826,14 +1008,16 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
826
1008
|
options.signal?.throwIfAborted();
|
|
827
1009
|
return value;
|
|
828
1010
|
}));
|
|
829
|
-
const stored =
|
|
1011
|
+
const stored = opts.heldResults?.stored(args.id) ??
|
|
1012
|
+
(yield* upwardStep(authCall(read, [resultKeys.chunk(args.id, 0)])));
|
|
830
1013
|
if (stored === false)
|
|
831
1014
|
return unavailableResult();
|
|
832
1015
|
if (stored === null) {
|
|
833
1016
|
return toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
|
|
834
1017
|
}
|
|
835
1018
|
const denied = () => toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
|
|
836
|
-
const
|
|
1019
|
+
const refused = RESULT_REFUSED_V4_HEADER.exec(stored);
|
|
1020
|
+
const header = refused ?? RESULT_ENVELOPE_V4_HEADER.exec(stored);
|
|
837
1021
|
if (!header)
|
|
838
1022
|
return denied();
|
|
839
1023
|
let binding;
|
|
@@ -863,6 +1047,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
863
1047
|
!["read", "write"].includes(dependency.classification))) ||
|
|
864
1048
|
!["read", "write"].includes(binding.classification))
|
|
865
1049
|
return denied();
|
|
1050
|
+
if (refused)
|
|
1051
|
+
return toolError(resultNotRetained(binding.classification === "write", args.id));
|
|
866
1052
|
const allowed = authMethod(() => authFlow(function* () {
|
|
867
1053
|
options.onStage?.("access validation");
|
|
868
1054
|
options.signal?.throwIfAborted();
|
|
@@ -1018,13 +1204,13 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
1018
1204
|
// lives once in the server instructions. A client that drops instructions
|
|
1019
1205
|
// still routes correctly from these scopes plus execute_code's terse lead.
|
|
1020
1206
|
// Only the meta surface lists these tools, so they use its names.
|
|
1021
|
-
const SEARCH_DESC = (readOnly) => `${readOnly ? "
|
|
1022
|
-
const CALL_DESC = (readOnly) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${
|
|
1023
|
-
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.";
|
|
1024
1210
|
/** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
|
|
1025
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.`;
|
|
1026
|
-
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
|
|
1027
|
-
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.";
|
|
1028
1214
|
/**
|
|
1029
1215
|
* Sentences appended to a meta-tool description only when this connection
|
|
1030
1216
|
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
@@ -1034,9 +1220,9 @@ const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the
|
|
|
1034
1220
|
* Registration is per connection and reads the configured connector set.
|
|
1035
1221
|
*/
|
|
1036
1222
|
const GUIDE_NOTES = {
|
|
1037
|
-
skills: "
|
|
1038
|
-
search: "
|
|
1039
|
-
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.",
|
|
1040
1226
|
};
|
|
1041
1227
|
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
1042
1228
|
function describedFor(registry, base, note) {
|
|
@@ -1061,7 +1247,10 @@ const READ_ONLY_LOCAL = {
|
|
|
1061
1247
|
openWorldHint: false,
|
|
1062
1248
|
};
|
|
1063
1249
|
const CALL_INPUT_SCHEMA = {
|
|
1064
|
-
|
|
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" }),
|
|
1065
1254
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
1066
1255
|
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1067
1256
|
timeoutMs: z.number().int().positive().optional(),
|
|
@@ -1081,7 +1270,13 @@ const SEARCH_INPUT = advertisedSchema(z.object({
|
|
|
1081
1270
|
fullDescriptions: z.boolean().optional(),
|
|
1082
1271
|
includeSchemas: z.enum(["compact", "json", "typescript"]).optional(),
|
|
1083
1272
|
}));
|
|
1084
|
-
|
|
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));
|
|
1085
1280
|
const CALL_DESTRUCTIVE_INPUT = advertisedSchema(z.strictObject({
|
|
1086
1281
|
...CALL_INPUT_SCHEMA,
|
|
1087
1282
|
// Bounded above, but with no lower bound: a model that sends `""` or
|
|
@@ -1089,8 +1284,8 @@ const CALL_DESTRUCTIVE_INPUT = advertisedSchema(z.strictObject({
|
|
|
1089
1284
|
// call over a cosmetic field the host merely displays is the wrong
|
|
1090
1285
|
// trade. It is normalized to absent in the handler instead.
|
|
1091
1286
|
reason: z.string().max(500).optional(),
|
|
1092
|
-
}));
|
|
1093
|
-
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.";
|
|
1094
1289
|
const READ_RESULT_INPUT = advertisedSchema(z.strictObject({
|
|
1095
1290
|
id: z.string(),
|
|
1096
1291
|
offset: z.number().int().nonnegative().optional(),
|
|
@@ -1112,6 +1307,8 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
|
|
|
1112
1307
|
export function registerMetaTools(server, registry, ctx) {
|
|
1113
1308
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1114
1309
|
maxProgramResultChars: ctx.maxProgramResultChars,
|
|
1310
|
+
maxProgramHostCalls: ctx.maxProgramHostCalls,
|
|
1311
|
+
programTool: ctx.programTool,
|
|
1115
1312
|
routes: ctx.routes,
|
|
1116
1313
|
...(ctx.native && !ctx.native.executeCode ? { pagingTool: "read_result" } : {}),
|
|
1117
1314
|
...(ctx.native ? { activitySource: "native" } : {}),
|
|
@@ -1119,6 +1316,8 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1119
1316
|
requestScope: ctx.requestScope,
|
|
1120
1317
|
readOnly: ctx.readOnly,
|
|
1121
1318
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1319
|
+
responseCeilingMs: ctx.responseCeilingMs,
|
|
1320
|
+
requestStartedAt: ctx.requestStartedAt,
|
|
1122
1321
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1123
1322
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
1124
1323
|
activity: ctx.activity,
|
|
@@ -1130,6 +1329,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1130
1329
|
requestSignal: ctx.requestSignal,
|
|
1131
1330
|
defer: ctx.defer,
|
|
1132
1331
|
});
|
|
1332
|
+
const routeProgram = ctx.routes?.program ?? META_ROUTES.program;
|
|
1133
1333
|
const bindRequest = (request) => {
|
|
1134
1334
|
if (ctx.client)
|
|
1135
1335
|
bindMcpClient(request, ctx.client, ctx.activity);
|
|
@@ -1138,7 +1338,6 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1138
1338
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1139
1339
|
...(ctx.native ? { _meta: { "dev.connecta/surface": { surface: "native", ...ctx.native } } } : {}),
|
|
1140
1340
|
inputSchema: SKILLS_INPUT,
|
|
1141
|
-
outputSchema: SKILLS_OUTPUT,
|
|
1142
1341
|
annotations: READ_ONLY_LOCAL,
|
|
1143
1342
|
}, authMethod((args, request) => authFlow(function* () {
|
|
1144
1343
|
bindRequest(request);
|
|
@@ -1193,7 +1392,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1193
1392
|
});
|
|
1194
1393
|
}
|
|
1195
1394
|
server.registerTool("search_tools", {
|
|
1196
|
-
description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true), "search"),
|
|
1395
|
+
description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true, routeProgram), "search"),
|
|
1197
1396
|
inputSchema: SEARCH_INPUT,
|
|
1198
1397
|
outputSchema: SEARCH_OUTPUT,
|
|
1199
1398
|
annotations: READ_ONLY_REMOTE,
|
|
@@ -1202,7 +1401,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1202
1401
|
return yield* upwardStep(authInvoke(mt, "searchTools", [args]));
|
|
1203
1402
|
})));
|
|
1204
1403
|
server.registerTool("call_tool", {
|
|
1205
|
-
description: CALL_DESC(ctx.readOnly === true),
|
|
1404
|
+
description: CALL_DESC(ctx.readOnly === true, routeProgram),
|
|
1206
1405
|
inputSchema: CALL_INPUT,
|
|
1207
1406
|
// Default mode returns native content, so no structured result is promised.
|
|
1208
1407
|
// call_tool admits only tools that are themselves explicitly read-only;
|