@tool-bridge/server 0.17.0 → 0.19.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/dist/{airtable-NOSOMOVB.js → airtable-23JBIM6X.js} +4 -4
- package/dist/{alt_text_generator_ai-BHIJ4LW5.js → alt_text_generator_ai-ADWBTCIH.js} +4 -4
- package/dist/{amap-QRLP4TWZ.js → amap-CYQCYW2Z.js} +4 -4
- package/dist/{anthropic-IEOI36EV.js → anthropic-5P62HDNR.js} +4 -4
- package/dist/{apify-3CCYTQVK.js → apify-54RZUAGW.js} +4 -4
- package/dist/{brave_search-PZNYDQG7.js → brave_search-AHJ6FTD5.js} +4 -4
- package/dist/{chunk-YASUMV2K.js → chunk-B4P6YZFS.js} +1 -1
- package/dist/{chunk-TLNUIPFZ.js → chunk-CLEZ6YQ6.js} +2117 -190
- package/dist/{chunk-RVWZN3CI.js → chunk-OKRZPQ3R.js} +443 -139
- package/dist/{chunk-U5UHT7CX.js → chunk-SWBNHIO4.js} +1 -1
- package/dist/{chunk-WW2IMPQK.js → chunk-VLOIDU5U.js} +3 -3
- package/dist/{circleci-VSJ4O5FC.js → circleci-UM3NSQJ3.js} +4 -4
- package/dist/{clerk-CC2JZBZI.js → clerk-FE5LQ2CZ.js} +4 -4
- package/dist/{cloudflare_dns-PJEBSEYQ.js → cloudflare_dns-KEZABKUT.js} +4 -4
- package/dist/{cohere-KBACYSPF.js → cohere-4IPJADXP.js} +4 -4
- package/dist/{confluence-2PX3O7G5.js → confluence-56QEKNQI.js} +4 -4
- package/dist/{convertapi-BVOTD2PJ.js → convertapi-3QRM5K5T.js} +3 -3
- package/dist/{currencyapi-6ZQ2VNQ7.js → currencyapi-L6G4OND5.js} +4 -4
- package/dist/{deepseek-MB3WVVTB.js → deepseek-LGC6HBFG.js} +4 -4
- package/dist/{docker_hub-7CLQL2IJ.js → docker_hub-XDMK3BIU.js} +4 -4
- package/dist/{dropbox-VFYDVUNK.js → dropbox-4QARMFZU.js} +4 -4
- package/dist/{dub-QN3EETJY.js → dub-C6JSJKOT.js} +4 -4
- package/dist/{e2b-NKC3Y776.js → e2b-6A6TVACJ.js} +4 -4
- package/dist/{exa-KKZ6F25K.js → exa-SIWWW6AA.js} +4 -4
- package/dist/{fal_ai-25LYJD2O.js → fal_ai-3IJXOKSP.js} +4 -4
- package/dist/{feishu-NRT55B3Q.js → feishu-L4KDA4DC.js} +2 -2
- package/dist/{feishu_custom_bot-3W3XXUG4.js → feishu_custom_bot-SDM75H4P.js} +3 -3
- package/dist/{figma-4FLP35TC.js → figma-6DD436DN.js} +4 -4
- package/dist/{firehydrant-BLXPLZ3X.js → firehydrant-SYSGYB3R.js} +4 -4
- package/dist/{fixer-4OR62TGE.js → fixer-FZG7ASTF.js} +4 -4
- package/dist/{fly-MYCKOZZK.js → fly-HMRCZBUC.js} +4 -4
- package/dist/{front-3DFYVMF3.js → front-FAFSD7I3.js} +4 -4
- package/dist/{geocodio-CXMD6VKA.js → geocodio-4D3TWENM.js} +4 -4
- package/dist/{ghost-4UUE737W.js → ghost-OSO2FOZS.js} +4 -4
- package/dist/{github-WFS2ON2N.js → github-OR4I4RHE.js} +4 -4
- package/dist/{gmail-KW7FXA7A.js → gmail-5AOPJ63E.js} +4 -4
- package/dist/{googlecalendar-AMH5JQMC.js → googlecalendar-SOJIJUWH.js} +4 -4
- package/dist/{googledocs-R5VA6CKD.js → googledocs-QSKN5TLN.js} +3 -3
- package/dist/{grafana-IT6DWIVB.js → grafana-R3AIZM7M.js} +4 -4
- package/dist/{graphhopper-TYFDJZWC.js → graphhopper-4CR3QOAX.js} +4 -4
- package/dist/index.d.ts +53 -8
- package/dist/index.js +3 -3
- package/dist/{ip2proxy-GJ4HXVXO.js → ip2proxy-OBCG6X6J.js} +4 -4
- package/dist/{ipgeolocation_io-OAGR6PRF.js → ipgeolocation_io-NHH3DTQW.js} +4 -4
- package/dist/{ipqualityscore-BCKAMDGL.js → ipqualityscore-7POMCBEC.js} +4 -4
- package/dist/{jina_ai-N3NXRVIZ.js → jina_ai-YXVKROV4.js} +4 -4
- package/dist/{jira-CHLBKWLO.js → jira-LADO3VJU.js} +4 -4
- package/dist/{langsmith-OIKOTPY3.js → langsmith-XZ6UAQFU.js} +4 -4
- package/dist/{lemlist-VAKYPKS3.js → lemlist-P74GLNE7.js} +4 -4
- package/dist/{linear-Q6QTPJ5R.js → linear-CXJ7D37A.js} +3 -3
- package/dist/{logsnag-SQOQBTES.js → logsnag-EVVHWFLK.js} +4 -4
- package/dist/main.js +102 -102
- package/dist/{mattermost-G2KBEICM.js → mattermost-WG2MRGBD.js} +4 -4
- package/dist/{mem0-D2FPLYDX.js → mem0-HQWUKG6U.js} +4 -4
- package/dist/{memos-76PTWV5V.js → memos-SQMJIFOE.js} +4 -4
- package/dist/{metabase-TWSIDVBM.js → metabase-E27G2DZI.js} +4 -4
- package/dist/{mistral_ai-TP7TQUEJ.js → mistral_ai-MIL63NIT.js} +4 -4
- package/dist/{mother_duck-UXY5THYU.js → mother_duck-AIZVCBNR.js} +4 -4
- package/dist/{ngrok-RZW2ISJK.js → ngrok-XS4DVIQS.js} +4 -4
- package/dist/{notes-B2I6FWK2.js → notes-JEOUSL2Y.js} +1 -1
- package/dist/{notion-QOKRUE6M.js → notion-U4EZVDW2.js} +4 -4
- package/dist/{open_exchange_rates-ODLFWEJ7.js → open_exchange_rates-AZSQEYEJ.js} +4 -4
- package/dist/{openai-ZPBPPPGT.js → openai-HW3SYMTQ.js} +4 -4
- package/dist/{opengraph_io-EOI5UJKT.js → opengraph_io-QX74MAEL.js} +4 -4
- package/dist/{openrouter-BUWLF3BG.js → openrouter-OXSKYL6N.js} +4 -4
- package/dist/{outline-NXS3QRXO.js → outline-CCQAPDSW.js} +4 -4
- package/dist/{paddle-T4MSKQ76.js → paddle-6KQPSSCK.js} +4 -4
- package/dist/{perplexity-WW5N2LW7.js → perplexity-2TXO7IQE.js} +4 -4
- package/dist/{pinecone-RY5AZPQM.js → pinecone-YOO7NULI.js} +4 -4
- package/dist/{pipedrive-OHYQNZTU.js → pipedrive-Q6RIFIFP.js} +4 -4
- package/dist/{polar-XZIAHXS5.js → polar-KUMGCES3.js} +4 -4
- package/dist/{posthog-DTPTWORY.js → posthog-VXQHBSMB.js} +4 -4
- package/dist/{postmark-F4UCQKC3.js → postmark-YLIPF52S.js} +4 -4
- package/dist/{prerender-P3Y2MLIC.js → prerender-CRY7UJN5.js} +4 -4
- package/dist/{pubmed-RHU3GXFB.js → pubmed-3Z4T5AM7.js} +3 -3
- package/dist/{railway-VB6SRLI6.js → railway-BP37IGFL.js} +3 -3
- package/dist/{readwise-DIMSKJZR.js → readwise-HTKICF4K.js} +4 -4
- package/dist/{render-XCCDCEEK.js → render-WIVTUXJH.js} +4 -4
- package/dist/{replicate-IUEOQ4JC.js → replicate-JJYPA43D.js} +4 -4
- package/dist/{resend-N5MJZH5E.js → resend-TKHZVG7A.js} +4 -4
- package/dist/{rootly-25T4NBTU.js → rootly-FRSZLG5U.js} +4 -4
- package/dist/{scrapfly-PK4XFXUA.js → scrapfly-M5WXGQWE.js} +4 -4
- package/dist/{scrapingbee-ITPBMWQU.js → scrapingbee-PMXNLLBK.js} +4 -4
- package/dist/{screenshot_fyi-5U6ZYPKR.js → screenshot_fyi-HFW4TAOH.js} +4 -4
- package/dist/{semantic_scholar-BJGCI2MU.js → semantic_scholar-KXVH2VZQ.js} +4 -4
- package/dist/{sentry-63IC263P.js → sentry-WWOT2LRX.js} +4 -4
- package/dist/{shodan-2WBIFHFN.js → shodan-FY635H35.js} +4 -4
- package/dist/{shopify-XLLJRAGM.js → shopify-HQLBVSA5.js} +4 -4
- package/dist/{stripe-WLVCY3UN.js → stripe-GJN2GOHB.js} +3 -3
- package/dist/{supabase-BKNZ35ER.js → supabase-BBNZGQU6.js} +4 -4
- package/dist/{tavily-YFSDEUTL.js → tavily-DPENIBOG.js} +4 -4
- package/dist/{telegram-FC35W74G.js → telegram-L4NONG6H.js} +4 -4
- package/dist/{telnyx-MI32XZUG.js → telnyx-H67MAEZS.js} +4 -4
- package/dist/{todoist-4ZFJKZC6.js → todoist-G2BSP4DX.js} +4 -4
- package/dist/{trello-UOGFNMK4.js → trello-7WMVFDG2.js} +4 -4
- package/dist/{turso-YTSTW3ZS.js → turso-ZUAESFC2.js} +4 -4
- package/dist/{twilio-XAJDFUQH.js → twilio-EYFYIX3B.js} +3 -3
- package/dist/{umami-ZVIUBPAI.js → umami-AXAUDUVF.js} +4 -4
- package/dist/{unsplash-IU3PLDNS.js → unsplash-UCQAWPLR.js} +4 -4
- package/dist/{upstash_redis-3EYJW7SY.js → upstash_redis-ZPZ4ASU6.js} +4 -4
- package/dist/{uptimerobot-PJG2ASRU.js → uptimerobot-4QVKKUD5.js} +3 -3
- package/dist/{v2ex-KFQWWGFC.js → v2ex-VGEFJTYC.js} +4 -4
- package/dist/{vercel-I2ZDOQRL.js → vercel-JME4TSCN.js} +4 -4
- package/dist/{wolfram_alpha_api-BNXOJMB6.js → wolfram_alpha_api-TRGU2MDG.js} +4 -4
- package/dist/{wordpress-36D7BSMP.js → wordpress-WRUKLCZR.js} +4 -4
- package/dist/{workos-6X4JJRRC.js → workos-ZDW4EHS4.js} +4 -4
- package/dist/{zhihu-CIXKCHIU.js → zhihu-4VI67CWU.js} +4 -4
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createGuardedFetch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SWBNHIO4.js";
|
|
4
4
|
import {
|
|
5
5
|
AnnotationStore,
|
|
6
6
|
CONTEXT_CAPABILITIES,
|
|
@@ -40,9 +40,11 @@ import {
|
|
|
40
40
|
TB_ERROR_CODES,
|
|
41
41
|
TOOL_SEARCH_AUDIT_NODE_LIMIT,
|
|
42
42
|
TOOL_SEARCH_BATCH_LIMIT,
|
|
43
|
+
TOOL_SEARCH_DESCRIPTION_BYTES_MAX,
|
|
43
44
|
TOOL_SEARCH_INSERT_SNAPSHOT_SQL,
|
|
44
45
|
TOOL_SEARCH_INSERT_SQL,
|
|
45
46
|
TOOL_SEARCH_PAGE_BYTES,
|
|
47
|
+
TOOL_SEARCH_RANKING_VERSION,
|
|
46
48
|
TOOL_SEARCH_SCHEMA_STATEMENTS,
|
|
47
49
|
TOOL_SEARCH_WORK_LIMIT,
|
|
48
50
|
ZodIssueCode,
|
|
@@ -124,6 +126,7 @@ import {
|
|
|
124
126
|
normalizeEntryPath,
|
|
125
127
|
normalizePath,
|
|
126
128
|
normalizeToolSearchLimit,
|
|
129
|
+
normalizeToolSearchOptions,
|
|
127
130
|
normalizeToolSearchPath,
|
|
128
131
|
normalizeUpstreamError,
|
|
129
132
|
number,
|
|
@@ -146,6 +149,7 @@ import {
|
|
|
146
149
|
pgSearchDialect,
|
|
147
150
|
pluginTokenSecretName,
|
|
148
151
|
prefixUpperBound,
|
|
152
|
+
prepareToolSearchQuery,
|
|
149
153
|
preprocess,
|
|
150
154
|
readinessResponseSchema,
|
|
151
155
|
record,
|
|
@@ -169,8 +173,11 @@ import {
|
|
|
169
173
|
toJSONSchema,
|
|
170
174
|
toolHelpModel,
|
|
171
175
|
toolSearchPageSchema,
|
|
176
|
+
toolSearchRelevanceSchema,
|
|
172
177
|
toolSearchRequestSchema,
|
|
173
178
|
toolSearchSnapshotDigest,
|
|
179
|
+
toolSearchSourceResultSchema,
|
|
180
|
+
toolSearchSourceSchema,
|
|
174
181
|
toolsToHelpModel,
|
|
175
182
|
treeJsonSchema,
|
|
176
183
|
union,
|
|
@@ -178,7 +185,7 @@ import {
|
|
|
178
185
|
url,
|
|
179
186
|
validatePath,
|
|
180
187
|
virtualizeTools
|
|
181
|
-
} from "./chunk-
|
|
188
|
+
} from "./chunk-OKRZPQ3R.js";
|
|
182
189
|
|
|
183
190
|
// src/config.ts
|
|
184
191
|
var DEFAULT_PORT = 8787;
|
|
@@ -10023,13 +10030,13 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
10023
10030
|
const buffer = [];
|
|
10024
10031
|
let endipv6Encountered = false;
|
|
10025
10032
|
let endIpv6 = false;
|
|
10026
|
-
let
|
|
10033
|
+
let consume2 = consumeHextets;
|
|
10027
10034
|
for (let i = 0; i < input.length; i++) {
|
|
10028
10035
|
const cursor = input[i];
|
|
10029
10036
|
if (cursor === "[" || cursor === "]") continue;
|
|
10030
10037
|
if (cursor === ":") {
|
|
10031
10038
|
if (endipv6Encountered === true) endIpv6 = true;
|
|
10032
|
-
if (!
|
|
10039
|
+
if (!consume2(buffer, address, output)) break;
|
|
10033
10040
|
if (++tokenCount > 7) {
|
|
10034
10041
|
output.error = true;
|
|
10035
10042
|
break;
|
|
@@ -10038,14 +10045,14 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
10038
10045
|
address.push(":");
|
|
10039
10046
|
continue;
|
|
10040
10047
|
} else if (cursor === "%") {
|
|
10041
|
-
if (!
|
|
10042
|
-
|
|
10048
|
+
if (!consume2(buffer, address, output)) break;
|
|
10049
|
+
consume2 = consumeIsZone;
|
|
10043
10050
|
} else {
|
|
10044
10051
|
buffer.push(cursor);
|
|
10045
10052
|
continue;
|
|
10046
10053
|
}
|
|
10047
10054
|
}
|
|
10048
|
-
if (buffer.length) if (
|
|
10055
|
+
if (buffer.length) if (consume2 === consumeIsZone) output.zone = buffer.join("");
|
|
10049
10056
|
else if (endIpv6) address.push(buffer.join(""));
|
|
10050
10057
|
else address.push(stringArrayToHexStripped(buffer));
|
|
10051
10058
|
output.address = address.join("");
|
|
@@ -19188,12 +19195,51 @@ async function invalidateMcpOAuth(store, nodePath) {
|
|
|
19188
19195
|
await store.delete(KEY_OAUTH_TOKENS + nodePath);
|
|
19189
19196
|
await store.delete(KEY_OAUTH_DISCOVERY + nodePath);
|
|
19190
19197
|
}
|
|
19198
|
+
async function assertMcpOAuthConfig(config, secrets) {
|
|
19199
|
+
if (config === null || typeof config !== "object") return;
|
|
19200
|
+
const candidate = config;
|
|
19201
|
+
if (candidate.kind !== "mcp" || candidate.oauthClient === void 0) return;
|
|
19202
|
+
if (candidate.auth !== "oauth") {
|
|
19203
|
+
throw new TBError("invalid_argument", "mcp oauthClient \u53EA\u80FD\u4E0E auth:'oauth' \u4E00\u8D77\u4F7F\u7528");
|
|
19204
|
+
}
|
|
19205
|
+
if (candidate.oauthClient === null || typeof candidate.oauthClient !== "object") {
|
|
19206
|
+
throw new TBError("invalid_argument", "mcp oauthClient \u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
19207
|
+
}
|
|
19208
|
+
const client = candidate.oauthClient;
|
|
19209
|
+
const unknown2 = Object.keys(client).filter((key) => !["clientId", "clientSecretRef"].includes(key));
|
|
19210
|
+
if (unknown2.length > 0) {
|
|
19211
|
+
throw new TBError(
|
|
19212
|
+
"invalid_argument",
|
|
19213
|
+
`mcp oauthClient \u4E0D\u63A5\u53D7\u5B57\u6BB5:${unknown2.join(", ")};client secret \u5FC5\u987B\u8D70 clientSecretRef`
|
|
19214
|
+
);
|
|
19215
|
+
}
|
|
19216
|
+
if (typeof client.clientId !== "string" || client.clientId.trim() === "") {
|
|
19217
|
+
throw new TBError("invalid_argument", "mcp oauthClient.clientId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
19218
|
+
}
|
|
19219
|
+
if (client.clientSecretRef === void 0) return;
|
|
19220
|
+
if (typeof client.clientSecretRef !== "string" || client.clientSecretRef.trim() === "") {
|
|
19221
|
+
throw new TBError("invalid_argument", "mcp oauthClient.clientSecretRef \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
19222
|
+
}
|
|
19223
|
+
if (await secrets.resolve(client.clientSecretRef) === void 0) {
|
|
19224
|
+
throw new TBError(
|
|
19225
|
+
"invalid_argument",
|
|
19226
|
+
`OAuth client secret ref '${client.clientSecretRef}' \u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u89E3\u5BC6`
|
|
19227
|
+
);
|
|
19228
|
+
}
|
|
19229
|
+
}
|
|
19191
19230
|
function reauthorizeRequired(nodePath) {
|
|
19192
19231
|
return new TBError(
|
|
19193
19232
|
"permission_denied",
|
|
19194
19233
|
`mcp upstream '${nodePath}' requires (re)authorization: run \`tb tool auth ${nodePath}\``
|
|
19195
19234
|
);
|
|
19196
19235
|
}
|
|
19236
|
+
var mcpOAuthFetch = (input, init) => {
|
|
19237
|
+
const headers = new Headers(
|
|
19238
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
19239
|
+
);
|
|
19240
|
+
const tokenRequest = headers.get("content-type")?.toLowerCase().startsWith("application/x-www-form-urlencoded") === true;
|
|
19241
|
+
return fetch(input, tokenRequest ? { ...init, redirect: "error" } : init);
|
|
19242
|
+
};
|
|
19197
19243
|
async function stateCryptoKey(secret) {
|
|
19198
19244
|
const digest = await crypto.subtle.digest(
|
|
19199
19245
|
"SHA-256",
|
|
@@ -19225,7 +19271,7 @@ async function openOAuthState(state, secret) {
|
|
|
19225
19271
|
ciphertext
|
|
19226
19272
|
);
|
|
19227
19273
|
const payload = JSON.parse(new TextDecoder().decode(plain));
|
|
19228
|
-
if (typeof payload.p !== "string" || typeof payload.v !== "string" || typeof payload.exp !== "number" || payload.r !== void 0 && typeof payload.r !== "string") {
|
|
19274
|
+
if (typeof payload.p !== "string" || typeof payload.v !== "string" || typeof payload.exp !== "number" || payload.r !== void 0 && typeof payload.r !== "string" || payload.o !== void 0 && (typeof payload.o !== "object" || payload.o === null || typeof payload.o.i !== "string" || payload.o.r !== void 0 && typeof payload.o.r !== "string")) {
|
|
19229
19275
|
return null;
|
|
19230
19276
|
}
|
|
19231
19277
|
return payload;
|
|
@@ -19252,11 +19298,31 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19252
19298
|
redirect_uris: [this.redirectUrl],
|
|
19253
19299
|
grant_types: ["authorization_code", "refresh_token"],
|
|
19254
19300
|
response_types: ["code"],
|
|
19255
|
-
|
|
19256
|
-
//
|
|
19301
|
+
// DCR 与 static public client 都是 PKCE public client;confidential client 由 AS
|
|
19302
|
+
// metadata + client_secret 自动协商 basic/post,不在 metadata 里误声明 none。
|
|
19303
|
+
...this.opts.oauthClient?.clientSecretRef === void 0 ? { token_endpoint_auth_method: "none" } : {}
|
|
19257
19304
|
};
|
|
19258
19305
|
}
|
|
19259
|
-
async clientInformation() {
|
|
19306
|
+
async clientInformation(ctx) {
|
|
19307
|
+
const configured = this.opts.oauthClient;
|
|
19308
|
+
if (configured !== void 0) {
|
|
19309
|
+
let clientSecret;
|
|
19310
|
+
if (configured.clientSecretRef !== void 0) {
|
|
19311
|
+
clientSecret = await this.opts.secrets.resolve(configured.clientSecretRef);
|
|
19312
|
+
if (clientSecret === void 0) {
|
|
19313
|
+
throw new TBError(
|
|
19314
|
+
"unavailable",
|
|
19315
|
+
`OAuth client secret ref '${configured.clientSecretRef}' \u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u89E3\u5BC6`,
|
|
19316
|
+
{ retryable: false }
|
|
19317
|
+
);
|
|
19318
|
+
}
|
|
19319
|
+
}
|
|
19320
|
+
return {
|
|
19321
|
+
client_id: configured.clientId,
|
|
19322
|
+
...clientSecret !== void 0 ? { client_secret: clientSecret } : {},
|
|
19323
|
+
...ctx?.issuer !== void 0 ? { issuer: ctx.issuer } : {}
|
|
19324
|
+
};
|
|
19325
|
+
}
|
|
19260
19326
|
const raw = await this.opts.store.get(KEY_OAUTH_CLIENT + this.opts.nodePath);
|
|
19261
19327
|
if (raw === null) {
|
|
19262
19328
|
if (this.opts.mode === "deny") throw reauthorizeRequired(this.opts.nodePath);
|
|
@@ -19265,14 +19331,47 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19265
19331
|
return raw;
|
|
19266
19332
|
}
|
|
19267
19333
|
async saveClientInformation(info) {
|
|
19334
|
+
if (this.opts.oauthClient !== void 0) return;
|
|
19268
19335
|
await this.opts.store.put(KEY_OAUTH_CLIENT + this.opts.nodePath, info);
|
|
19269
19336
|
}
|
|
19270
|
-
async tokens() {
|
|
19337
|
+
async tokens(ctx) {
|
|
19271
19338
|
const raw = await this.opts.store.get(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19272
|
-
|
|
19339
|
+
if (raw === null) return void 0;
|
|
19340
|
+
const tokens = raw;
|
|
19341
|
+
const discovery = await this.opts.store.get(KEY_OAUTH_DISCOVERY + this.opts.nodePath);
|
|
19342
|
+
const discoveredIssuer = discovery?.authorizationServerMetadata?.issuer;
|
|
19343
|
+
if (discoveredIssuer !== void 0 && tokens.issuer !== void 0 && tokens.issuer !== discoveredIssuer) {
|
|
19344
|
+
await this.opts.store.delete(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19345
|
+
return void 0;
|
|
19346
|
+
}
|
|
19347
|
+
let binding = tokens.toolBridgeBinding;
|
|
19348
|
+
const client = await this.clientInformation(ctx);
|
|
19349
|
+
if (binding === void 0 && this.opts.oauthClient === void 0 && client !== void 0 && "redirect_uris" in client && client.redirect_uris?.length === 1) {
|
|
19350
|
+
binding = { clientId: client.client_id, redirectUri: client.redirect_uris[0] };
|
|
19351
|
+
await this.opts.store.put(KEY_OAUTH_TOKENS + this.opts.nodePath, {
|
|
19352
|
+
...tokens,
|
|
19353
|
+
toolBridgeBinding: binding
|
|
19354
|
+
});
|
|
19355
|
+
}
|
|
19356
|
+
const bindingMismatch = binding === void 0 || client === void 0 || binding.clientId !== client.client_id || this.opts.mode === "interactive" && binding.redirectUri !== this.redirectUrl;
|
|
19357
|
+
if (bindingMismatch) {
|
|
19358
|
+
await this.opts.store.delete(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19359
|
+
return void 0;
|
|
19360
|
+
}
|
|
19361
|
+
return tokens;
|
|
19273
19362
|
}
|
|
19274
|
-
async saveTokens(tokens) {
|
|
19275
|
-
await this.
|
|
19363
|
+
async saveTokens(tokens, ctx) {
|
|
19364
|
+
const client = await this.clientInformation(ctx);
|
|
19365
|
+
if (client === void 0) throw new TBError("internal", "OAuth client information missing");
|
|
19366
|
+
const previous = await this.opts.store.get(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19367
|
+
const redirectUri = this.opts.mode === "interactive" ? this.redirectUrl : previous?.toolBridgeBinding?.redirectUri ?? ("redirect_uris" in client && client.redirect_uris?.length === 1 ? client.redirect_uris[0] : void 0);
|
|
19368
|
+
if (redirectUri === void 0) {
|
|
19369
|
+
throw new TBError("permission_denied", "OAuth grant redirect binding missing; reauthorize");
|
|
19370
|
+
}
|
|
19371
|
+
await this.opts.store.put(KEY_OAUTH_TOKENS + this.opts.nodePath, {
|
|
19372
|
+
...tokens,
|
|
19373
|
+
toolBridgeBinding: { clientId: client.client_id, redirectUri }
|
|
19374
|
+
});
|
|
19276
19375
|
}
|
|
19277
19376
|
async saveCodeVerifier(codeVerifier) {
|
|
19278
19377
|
this.verifier = codeVerifier;
|
|
@@ -19294,7 +19393,13 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19294
19393
|
p: this.opts.nodePath,
|
|
19295
19394
|
v: this.codeVerifier(),
|
|
19296
19395
|
exp: Math.floor(Date.now() / 1e3) + STATE_TTL_SEC,
|
|
19297
|
-
|
|
19396
|
+
r: this.redirectUrl,
|
|
19397
|
+
...this.opts.oauthClient !== void 0 ? {
|
|
19398
|
+
o: {
|
|
19399
|
+
i: this.opts.oauthClient.clientId,
|
|
19400
|
+
...this.opts.oauthClient.clientSecretRef !== void 0 ? { r: this.opts.oauthClient.clientSecretRef } : {}
|
|
19401
|
+
}
|
|
19402
|
+
} : {}
|
|
19298
19403
|
},
|
|
19299
19404
|
this.opts.encryptionKey
|
|
19300
19405
|
);
|
|
@@ -19311,7 +19416,9 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19311
19416
|
/** SDK 对 InvalidClient/InvalidGrant 等可恢复错误的重试钩子:清对应缓存后重走。 */
|
|
19312
19417
|
async invalidateCredentials(scope) {
|
|
19313
19418
|
const { store, nodePath } = this.opts;
|
|
19314
|
-
if (scope === "all" || scope === "client")
|
|
19419
|
+
if ((scope === "all" || scope === "client") && this.opts.oauthClient === void 0) {
|
|
19420
|
+
await store.delete(KEY_OAUTH_CLIENT + nodePath);
|
|
19421
|
+
}
|
|
19315
19422
|
if (scope === "all" || scope === "tokens") await store.delete(KEY_OAUTH_TOKENS + nodePath);
|
|
19316
19423
|
if (scope === "all" || scope === "discovery") await store.delete(KEY_OAUTH_DISCOVERY + nodePath);
|
|
19317
19424
|
if (scope === "all" || scope === "verifier") this.verifier = void 0;
|
|
@@ -19338,6 +19445,13 @@ async function guardOAuth(fn) {
|
|
|
19338
19445
|
return await fn();
|
|
19339
19446
|
} catch (err) {
|
|
19340
19447
|
if (err instanceof TBError) throw err;
|
|
19448
|
+
if (err instanceof Error && /does not support dynamic client registration/i.test(err.message)) {
|
|
19449
|
+
throw new TBError(
|
|
19450
|
+
"invalid_argument",
|
|
19451
|
+
"authorization server \u672A\u63D0\u4F9B\u52A8\u6001\u5BA2\u6237\u7AEF\u6CE8\u518C\uFF1B\u8BF7\u914D\u7F6E\u9884\u6CE8\u518C OAuth client(CLI: --oauth-client-id\uFF0C\u53EF\u9009 --oauth-client-secret-ref)",
|
|
19452
|
+
{ retryable: false }
|
|
19453
|
+
);
|
|
19454
|
+
}
|
|
19341
19455
|
throw new TBError(
|
|
19342
19456
|
"unavailable",
|
|
19343
19457
|
`OAuth flow failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -19346,11 +19460,13 @@ async function guardOAuth(fn) {
|
|
|
19346
19460
|
}
|
|
19347
19461
|
}
|
|
19348
19462
|
async function startMcpAuthorization(opts) {
|
|
19349
|
-
if (opts.redirectUri !== void 0)
|
|
19350
|
-
|
|
19463
|
+
if (opts.redirectUri !== void 0) assertLocalRedirectUri(opts.redirectUri);
|
|
19464
|
+
const redirectUri = opts.redirectUri ?? `${opts.origin}${OAUTH_CALLBACK_PATH}`;
|
|
19465
|
+
if (opts.oauthClient === void 0) {
|
|
19351
19466
|
const cached = await opts.store.get(KEY_OAUTH_CLIENT + opts.nodePath);
|
|
19352
|
-
if (cached !== null && !(cached.redirect_uris ?? []).includes(
|
|
19467
|
+
if (cached !== null && !(cached.redirect_uris ?? []).includes(redirectUri)) {
|
|
19353
19468
|
await opts.store.delete(KEY_OAUTH_CLIENT + opts.nodePath);
|
|
19469
|
+
await opts.store.delete(KEY_OAUTH_TOKENS + opts.nodePath);
|
|
19354
19470
|
}
|
|
19355
19471
|
}
|
|
19356
19472
|
const provider = new GatewayMcpOAuthProvider({
|
|
@@ -19359,10 +19475,12 @@ async function startMcpAuthorization(opts) {
|
|
|
19359
19475
|
encryptionKey: opts.encryptionKey,
|
|
19360
19476
|
mode: "interactive",
|
|
19361
19477
|
origin: opts.origin,
|
|
19478
|
+
secrets: opts.secrets,
|
|
19479
|
+
...opts.oauthClient !== void 0 ? { oauthClient: opts.oauthClient } : {},
|
|
19362
19480
|
...opts.redirectUri !== void 0 ? { redirectUri: opts.redirectUri } : {}
|
|
19363
19481
|
});
|
|
19364
19482
|
return await guardOAuth(async () => {
|
|
19365
|
-
const result = await auth(provider, { serverUrl: opts.serverUrl });
|
|
19483
|
+
const result = await auth(provider, { serverUrl: opts.serverUrl, fetchFn: mcpOAuthFetch });
|
|
19366
19484
|
if (result === "AUTHORIZED") return { status: "authorized" };
|
|
19367
19485
|
const url2 = provider.capturedAuthorizationUrl;
|
|
19368
19486
|
if (url2 === void 0) {
|
|
@@ -19378,13 +19496,17 @@ async function finishMcpAuthorization(opts) {
|
|
|
19378
19496
|
encryptionKey: opts.encryptionKey,
|
|
19379
19497
|
mode: "interactive",
|
|
19380
19498
|
origin: opts.origin,
|
|
19499
|
+
secrets: opts.secrets,
|
|
19500
|
+
...opts.oauthClient !== void 0 ? { oauthClient: opts.oauthClient } : {},
|
|
19381
19501
|
...opts.redirectUri !== void 0 ? { redirectUri: opts.redirectUri } : {}
|
|
19382
19502
|
});
|
|
19383
19503
|
provider.setCodeVerifier(opts.codeVerifier);
|
|
19384
19504
|
await guardOAuth(async () => {
|
|
19385
19505
|
const result = await auth(provider, {
|
|
19386
19506
|
serverUrl: opts.serverUrl,
|
|
19387
|
-
authorizationCode: opts.code
|
|
19507
|
+
authorizationCode: opts.code,
|
|
19508
|
+
fetchFn: mcpOAuthFetch,
|
|
19509
|
+
...opts.authorizationResponseIssuer !== void 0 ? { iss: opts.authorizationResponseIssuer } : {}
|
|
19388
19510
|
});
|
|
19389
19511
|
if (result !== "AUTHORIZED") {
|
|
19390
19512
|
throw new TBError("internal", "authorization did not complete");
|
|
@@ -19451,7 +19573,7 @@ var noStandaloneSseFetch = (input, init) => {
|
|
|
19451
19573
|
if (String(method).toUpperCase() === "GET" && accept.includes("text/event-stream")) {
|
|
19452
19574
|
return Promise.resolve(new Response(null, { status: 405, statusText: "Method Not Allowed" }));
|
|
19453
19575
|
}
|
|
19454
|
-
return
|
|
19576
|
+
return mcpOAuthFetch(input, init);
|
|
19455
19577
|
};
|
|
19456
19578
|
var ERA_KEY_PREFIX = "mcpera:";
|
|
19457
19579
|
var LEGACY_ERA_HORIZON_SEC = 300;
|
|
@@ -19591,7 +19713,9 @@ function createMcpProvider(config, secrets, opts) {
|
|
|
19591
19713
|
store: opts.oauth.store,
|
|
19592
19714
|
nodePath,
|
|
19593
19715
|
encryptionKey: opts.oauth.encryptionKey,
|
|
19594
|
-
mode: "deny"
|
|
19716
|
+
mode: "deny",
|
|
19717
|
+
secrets,
|
|
19718
|
+
...config.oauthClient !== void 0 ? { oauthClient: config.oauthClient } : {}
|
|
19595
19719
|
})
|
|
19596
19720
|
};
|
|
19597
19721
|
}
|
|
@@ -28157,13 +28281,13 @@ var require_utils2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
|
|
|
28157
28281
|
const buffer = [];
|
|
28158
28282
|
let endipv6Encountered = false;
|
|
28159
28283
|
let endIpv6 = false;
|
|
28160
|
-
let
|
|
28284
|
+
let consume2 = consumeHextets;
|
|
28161
28285
|
for (let i = 0; i < input.length; i++) {
|
|
28162
28286
|
const cursor = input[i];
|
|
28163
28287
|
if (cursor === "[" || cursor === "]") continue;
|
|
28164
28288
|
if (cursor === ":") {
|
|
28165
28289
|
if (endipv6Encountered === true) endIpv6 = true;
|
|
28166
|
-
if (!
|
|
28290
|
+
if (!consume2(buffer, address, output)) break;
|
|
28167
28291
|
if (++tokenCount > 7) {
|
|
28168
28292
|
output.error = true;
|
|
28169
28293
|
break;
|
|
@@ -28172,14 +28296,14 @@ var require_utils2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
|
|
|
28172
28296
|
address.push(":");
|
|
28173
28297
|
continue;
|
|
28174
28298
|
} else if (cursor === "%") {
|
|
28175
|
-
if (!
|
|
28176
|
-
|
|
28299
|
+
if (!consume2(buffer, address, output)) break;
|
|
28300
|
+
consume2 = consumeIsZone;
|
|
28177
28301
|
} else {
|
|
28178
28302
|
buffer.push(cursor);
|
|
28179
28303
|
continue;
|
|
28180
28304
|
}
|
|
28181
28305
|
}
|
|
28182
|
-
if (buffer.length) if (
|
|
28306
|
+
if (buffer.length) if (consume2 === consumeIsZone) output.zone = buffer.join("");
|
|
28183
28307
|
else if (endIpv6) address.push(buffer.join(""));
|
|
28184
28308
|
else address.push(stringArrayToHexStripped(buffer));
|
|
28185
28309
|
output.address = address.join("");
|
|
@@ -36350,6 +36474,121 @@ function createPluginContextProvider(opts) {
|
|
|
36350
36474
|
|
|
36351
36475
|
// ../app/src/providers/remote.ts
|
|
36352
36476
|
var VIA_HEADER = "x-tb-via";
|
|
36477
|
+
function remoteAbortError() {
|
|
36478
|
+
return new TBError("unavailable", "remote request aborted", { retryable: true });
|
|
36479
|
+
}
|
|
36480
|
+
function remoteResponseTooLarge(maxBytes) {
|
|
36481
|
+
return new TBError(
|
|
36482
|
+
"unavailable",
|
|
36483
|
+
`remote response body exceeds ${maxBytes} bytes`,
|
|
36484
|
+
{ retryable: false }
|
|
36485
|
+
);
|
|
36486
|
+
}
|
|
36487
|
+
function isAbortError(error, signal) {
|
|
36488
|
+
return signal?.aborted === true || error instanceof Error && error.name === "AbortError";
|
|
36489
|
+
}
|
|
36490
|
+
function signalAborted(signal) {
|
|
36491
|
+
return signal?.aborted === true;
|
|
36492
|
+
}
|
|
36493
|
+
function declaredContentLength(response) {
|
|
36494
|
+
const value = response.headers.get("content-length");
|
|
36495
|
+
if (value === null || !/^\d+$/.test(value)) return void 0;
|
|
36496
|
+
const parsed = Number(value);
|
|
36497
|
+
return Number.isSafeInteger(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
36498
|
+
}
|
|
36499
|
+
async function readRemoteResponseBody(response, maxBytes, signal) {
|
|
36500
|
+
if (signal?.aborted === true) throw remoteAbortError();
|
|
36501
|
+
const declared = declaredContentLength(response);
|
|
36502
|
+
if (maxBytes !== void 0 && declared !== void 0 && declared > maxBytes) {
|
|
36503
|
+
void response.body?.cancel().catch(() => {
|
|
36504
|
+
});
|
|
36505
|
+
throw remoteResponseTooLarge(maxBytes);
|
|
36506
|
+
}
|
|
36507
|
+
if (response.body === null) return new Uint8Array();
|
|
36508
|
+
const reader = response.body.getReader();
|
|
36509
|
+
const chunks = [];
|
|
36510
|
+
let total = 0;
|
|
36511
|
+
let abortListener;
|
|
36512
|
+
const abortPromise = signal === void 0 ? void 0 : new Promise((_resolve, reject) => {
|
|
36513
|
+
abortListener = () => {
|
|
36514
|
+
reject(remoteAbortError());
|
|
36515
|
+
void reader.cancel().catch(() => {
|
|
36516
|
+
});
|
|
36517
|
+
};
|
|
36518
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
36519
|
+
if (signal.aborted) abortListener();
|
|
36520
|
+
});
|
|
36521
|
+
try {
|
|
36522
|
+
while (true) {
|
|
36523
|
+
const result = await (abortPromise === void 0 ? reader.read() : Promise.race([reader.read(), abortPromise]));
|
|
36524
|
+
if (signalAborted(signal)) throw remoteAbortError();
|
|
36525
|
+
if (result.done) break;
|
|
36526
|
+
total += result.value.byteLength;
|
|
36527
|
+
if (maxBytes !== void 0 && total > maxBytes) {
|
|
36528
|
+
void reader.cancel().catch(() => {
|
|
36529
|
+
});
|
|
36530
|
+
throw remoteResponseTooLarge(maxBytes);
|
|
36531
|
+
}
|
|
36532
|
+
chunks.push(result.value);
|
|
36533
|
+
}
|
|
36534
|
+
} catch (error) {
|
|
36535
|
+
if (error instanceof TBError) throw error;
|
|
36536
|
+
if (isAbortError(error, signal)) throw remoteAbortError();
|
|
36537
|
+
throw normalizeUpstreamError({
|
|
36538
|
+
kind: "network",
|
|
36539
|
+
message: "failed to read remote response body"
|
|
36540
|
+
});
|
|
36541
|
+
} finally {
|
|
36542
|
+
if (abortListener !== void 0) signal?.removeEventListener("abort", abortListener);
|
|
36543
|
+
}
|
|
36544
|
+
const body = new Uint8Array(total);
|
|
36545
|
+
let offset = 0;
|
|
36546
|
+
for (const chunk of chunks) {
|
|
36547
|
+
body.set(chunk, offset);
|
|
36548
|
+
offset += chunk.byteLength;
|
|
36549
|
+
}
|
|
36550
|
+
return body;
|
|
36551
|
+
}
|
|
36552
|
+
var DEFAULT_FEDERATED_SEARCH_SETTINGS = {
|
|
36553
|
+
maxConcurrency: 4,
|
|
36554
|
+
maxResponseBodyBytes: 512 * 1024,
|
|
36555
|
+
maxSources: 16,
|
|
36556
|
+
minChildWorkMs: 200,
|
|
36557
|
+
perHopReturnReserveMs: 100,
|
|
36558
|
+
sessionTtlMs: 5 * 60 * 1e3,
|
|
36559
|
+
totalDeadlineMs: 2500
|
|
36560
|
+
};
|
|
36561
|
+
function federatedSearchSettings(settings) {
|
|
36562
|
+
const configured = settings.federatedSearch ?? {};
|
|
36563
|
+
const positive = (value, fallback) => value !== void 0 && Number.isFinite(value) && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
36564
|
+
return {
|
|
36565
|
+
maxConcurrency: positive(
|
|
36566
|
+
configured.maxConcurrency,
|
|
36567
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.maxConcurrency
|
|
36568
|
+
),
|
|
36569
|
+
maxResponseBodyBytes: positive(
|
|
36570
|
+
configured.maxResponseBodyBytes,
|
|
36571
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.maxResponseBodyBytes
|
|
36572
|
+
),
|
|
36573
|
+
maxSources: positive(configured.maxSources, DEFAULT_FEDERATED_SEARCH_SETTINGS.maxSources),
|
|
36574
|
+
minChildWorkMs: positive(
|
|
36575
|
+
configured.minChildWorkMs,
|
|
36576
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.minChildWorkMs
|
|
36577
|
+
),
|
|
36578
|
+
perHopReturnReserveMs: positive(
|
|
36579
|
+
configured.perHopReturnReserveMs,
|
|
36580
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.perHopReturnReserveMs
|
|
36581
|
+
),
|
|
36582
|
+
sessionTtlMs: positive(
|
|
36583
|
+
configured.sessionTtlMs,
|
|
36584
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.sessionTtlMs
|
|
36585
|
+
),
|
|
36586
|
+
totalDeadlineMs: positive(
|
|
36587
|
+
configured.totalDeadlineMs,
|
|
36588
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.totalDeadlineMs
|
|
36589
|
+
)
|
|
36590
|
+
};
|
|
36591
|
+
}
|
|
36353
36592
|
function selfInstanceId(settings, requestUrl) {
|
|
36354
36593
|
if (settings.instanceId !== void 0 && settings.instanceId.length > 0) {
|
|
36355
36594
|
return settings.instanceId;
|
|
@@ -36368,6 +36607,13 @@ function assertRemoteAllowed(baseUrl, settings) {
|
|
|
36368
36607
|
}
|
|
36369
36608
|
}
|
|
36370
36609
|
async function passthroughRemote(opts) {
|
|
36610
|
+
if (opts.maxResponseBodyBytes !== void 0 && (!Number.isSafeInteger(opts.maxResponseBodyBytes) || opts.maxResponseBodyBytes < 0)) {
|
|
36611
|
+
throw new TBError(
|
|
36612
|
+
"invalid_argument",
|
|
36613
|
+
"maxResponseBodyBytes must be a non-negative safe integer"
|
|
36614
|
+
);
|
|
36615
|
+
}
|
|
36616
|
+
if (opts.signal?.aborted === true) throw remoteAbortError();
|
|
36371
36617
|
const secErr = assertSecureUrl(opts.config.baseUrl, opts.settings.allowInsecure);
|
|
36372
36618
|
if (secErr) throw secErr;
|
|
36373
36619
|
if (!checkAllowlist(opts.config.baseUrl, opts.settings.allowlist)) {
|
|
@@ -36392,6 +36638,16 @@ async function passthroughRemote(opts) {
|
|
|
36392
36638
|
if (accept !== null) outHeaders.accept = accept;
|
|
36393
36639
|
const contentType = opts.headers.get("content-type");
|
|
36394
36640
|
if (contentType !== null) outHeaders["content-type"] = contentType;
|
|
36641
|
+
for (const name of [
|
|
36642
|
+
"x-tb-search-remaining-ms",
|
|
36643
|
+
"x-tb-search-session-ttl-ms",
|
|
36644
|
+
"x-tb-search-source-budget",
|
|
36645
|
+
"x-tb-search-validate-snapshot",
|
|
36646
|
+
"x-tb-search-want-snapshot"
|
|
36647
|
+
]) {
|
|
36648
|
+
const value = opts.headers.get(name);
|
|
36649
|
+
if (value !== null) outHeaders[name] = value;
|
|
36650
|
+
}
|
|
36395
36651
|
if (opts.config.skRef !== void 0) {
|
|
36396
36652
|
const cred = await opts.secrets.resolve(opts.config.skRef);
|
|
36397
36653
|
if (cred === void 0) {
|
|
@@ -36419,17 +36675,29 @@ async function passthroughRemote(opts) {
|
|
|
36419
36675
|
resp = await fetch(target, {
|
|
36420
36676
|
method: opts.method,
|
|
36421
36677
|
headers: outHeaders,
|
|
36678
|
+
// remote 请求可能携带高权 service credential 与敏感 arguments。禁止自动跟随任何
|
|
36679
|
+
// 30x,避免 redirect 绕过初始 baseUrl 的 allowlist/HTTPS 校验或把 body 发往另一目标。
|
|
36680
|
+
redirect: "error",
|
|
36681
|
+
...opts.signal === void 0 ? {} : { signal: opts.signal },
|
|
36422
36682
|
...opts.body !== void 0 && opts.body.length > 0 ? { body: opts.body } : {}
|
|
36423
36683
|
});
|
|
36424
36684
|
} catch (err) {
|
|
36685
|
+
if (isAbortError(err, opts.signal)) throw remoteAbortError();
|
|
36425
36686
|
throw normalizeUpstreamError({
|
|
36426
36687
|
kind: "network",
|
|
36427
36688
|
message: err instanceof Error ? err.message : String(err)
|
|
36428
36689
|
});
|
|
36429
36690
|
}
|
|
36430
|
-
const respBody = await
|
|
36691
|
+
const respBody = await readRemoteResponseBody(
|
|
36692
|
+
resp,
|
|
36693
|
+
opts.maxResponseBodyBytes,
|
|
36694
|
+
opts.signal
|
|
36695
|
+
);
|
|
36431
36696
|
const respCt = resp.headers.get("content-type") ?? "application/octet-stream";
|
|
36432
|
-
return new Response(respBody
|
|
36697
|
+
return new Response(respBody.buffer, {
|
|
36698
|
+
status: resp.status,
|
|
36699
|
+
headers: { "content-type": respCt }
|
|
36700
|
+
});
|
|
36433
36701
|
}
|
|
36434
36702
|
|
|
36435
36703
|
// ../app/src/tbApp.ts
|
|
@@ -36454,8 +36722,11 @@ var schemas = {
|
|
|
36454
36722
|
OAuthAuthorizeResponse: oauthAuthorizeResponseSchema,
|
|
36455
36723
|
Readiness: readinessResponseSchema,
|
|
36456
36724
|
RegistryNode: registryNodeSchema,
|
|
36725
|
+
SearchRelevance: toolSearchRelevanceSchema,
|
|
36457
36726
|
SearchRequest: toolSearchRequestSchema,
|
|
36458
36727
|
SearchResponse: toolSearchPageSchema,
|
|
36728
|
+
SearchSource: toolSearchSourceSchema,
|
|
36729
|
+
SearchSourceResult: toolSearchSourceResultSchema,
|
|
36459
36730
|
Tree: treeJsonSchema
|
|
36460
36731
|
};
|
|
36461
36732
|
for (const [id, schema] of Object.entries(schemas)) schemaRegistry.add(schema, { id });
|
|
@@ -36980,6 +37251,7 @@ function renderTreeDsl(tree) {
|
|
|
36980
37251
|
}
|
|
36981
37252
|
|
|
36982
37253
|
// ../app/src/federation.ts
|
|
37254
|
+
var SEARCH_REMAINING_MS_HEADER = "x-tb-search-remaining-ms";
|
|
36983
37255
|
function remoteProtocolError(message) {
|
|
36984
37256
|
return new TBError("unavailable", message, { retryable: false });
|
|
36985
37257
|
}
|
|
@@ -36987,28 +37259,26 @@ function canonicalRemotePath(path, allowRoot) {
|
|
|
36987
37259
|
if (path !== path.replace(/^\/+|\/+$/g, "")) {
|
|
36988
37260
|
throw remoteProtocolError(`remote returned non-canonical path '${path}'`);
|
|
36989
37261
|
}
|
|
37262
|
+
if (path !== path.toLowerCase()) {
|
|
37263
|
+
throw remoteProtocolError(`remote returned non-canonical path '${path}'`);
|
|
37264
|
+
}
|
|
36990
37265
|
const invalid = validatePath(path, { allowRoot });
|
|
36991
37266
|
if (invalid !== null) throw remoteProtocolError(`remote returned invalid path '${path}'`);
|
|
36992
37267
|
for (const segment of path === "" ? [] : path.split("/")) {
|
|
36993
|
-
|
|
36994
|
-
|
|
36995
|
-
|
|
36996
|
-
|
|
36997
|
-
|
|
36998
|
-
|
|
36999
|
-
|
|
37000
|
-
|
|
37001
|
-
|
|
37002
|
-
|
|
37003
|
-
|
|
37004
|
-
|
|
37005
|
-
|
|
37006
|
-
}
|
|
37007
|
-
if (next === decoded) break;
|
|
37008
|
-
if (pass === 3) {
|
|
37009
|
-
throw remoteProtocolError(`remote returned over-encoded path '${path}'`);
|
|
37010
|
-
}
|
|
37011
|
-
decoded = next;
|
|
37268
|
+
if (segment === "." || segment === ".." || segment.startsWith("~") || segment.includes("\\") || segment.includes("?") || segment.includes("#") || [...segment].some((char) => {
|
|
37269
|
+
const code = char.charCodeAt(0);
|
|
37270
|
+
return code <= 31 || code === 127;
|
|
37271
|
+
})) {
|
|
37272
|
+
throw remoteProtocolError(`remote returned unsafe path segment '${segment}'`);
|
|
37273
|
+
}
|
|
37274
|
+
let decoded;
|
|
37275
|
+
try {
|
|
37276
|
+
decoded = decodeURIComponent(segment);
|
|
37277
|
+
} catch {
|
|
37278
|
+
throw remoteProtocolError(`remote returned malformed encoded path '${path}'`);
|
|
37279
|
+
}
|
|
37280
|
+
if (decoded !== segment) {
|
|
37281
|
+
throw remoteProtocolError(`remote returned non-canonical encoded path '${path}'`);
|
|
37012
37282
|
}
|
|
37013
37283
|
}
|
|
37014
37284
|
return path;
|
|
@@ -37016,19 +37286,104 @@ function canonicalRemotePath(path, allowRoot) {
|
|
|
37016
37286
|
function remotePathWithin(nodePath, commandPath) {
|
|
37017
37287
|
return nodePath === "" || commandPath === nodePath || commandPath.startsWith(`${nodePath}/`);
|
|
37018
37288
|
}
|
|
37019
|
-
|
|
37020
|
-
|
|
37021
|
-
|
|
37022
|
-
|
|
37023
|
-
|
|
37289
|
+
var CALLABLE_TREE_KINDS = /* @__PURE__ */ new Set(["device", "http", "mcp", "remote", "tool"]);
|
|
37290
|
+
function visibleRemotePath(ctx, path, kind) {
|
|
37291
|
+
if (!check(ctx, path, "read").allow) return false;
|
|
37292
|
+
return !CALLABLE_TREE_KINDS.has(kind) || check(ctx, path, "call").allow;
|
|
37293
|
+
}
|
|
37294
|
+
var RemotePathProjector = class {
|
|
37295
|
+
constructor(mountPath) {
|
|
37296
|
+
this.mountPath = mountPath;
|
|
37297
|
+
const invalid = validatePath(mountPath);
|
|
37298
|
+
if (invalid !== null) throw remoteProtocolError(`invalid remote mount path '${mountPath}'`);
|
|
37299
|
+
}
|
|
37300
|
+
mountPath;
|
|
37301
|
+
childPath(localPath) {
|
|
37302
|
+
if (localPath === this.mountPath) return "";
|
|
37303
|
+
if (!localPath.startsWith(`${this.mountPath}/`)) {
|
|
37304
|
+
throw remoteProtocolError(`local path '${localPath}' escapes remote mount '${this.mountPath}'`);
|
|
37305
|
+
}
|
|
37306
|
+
return localPath.slice(this.mountPath.length + 1);
|
|
37307
|
+
}
|
|
37308
|
+
localPath(childPath, allowRoot = true) {
|
|
37309
|
+
const canonical = canonicalRemotePath(childPath, allowRoot);
|
|
37310
|
+
return canonical === "" ? this.mountPath : `${this.mountPath}/${canonical}`;
|
|
37311
|
+
}
|
|
37312
|
+
childCommandPath(path) {
|
|
37313
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
37314
|
+
throw remoteProtocolError(`remote ~help returned invalid command path '${path}'`);
|
|
37315
|
+
}
|
|
37316
|
+
return canonicalRemotePath(path.slice(1), false);
|
|
37317
|
+
}
|
|
37318
|
+
projectHelp(help, requestedLocalPath, ctx) {
|
|
37319
|
+
const expectedChildPath = this.childPath(requestedLocalPath);
|
|
37320
|
+
const nodeChildPath = canonicalRemotePath(help.node.path, expectedChildPath === "");
|
|
37321
|
+
if (nodeChildPath !== expectedChildPath) {
|
|
37322
|
+
throw remoteProtocolError(
|
|
37323
|
+
`remote ~help path '${nodeChildPath}' does not match request '${expectedChildPath}'`
|
|
37324
|
+
);
|
|
37325
|
+
}
|
|
37326
|
+
const nodePath = this.localPath(nodeChildPath, true);
|
|
37327
|
+
const cmds = help.cmds.map((command) => {
|
|
37328
|
+
const childCommandPath = this.childCommandPath(command.path);
|
|
37329
|
+
if (!remotePathWithin(nodeChildPath, childCommandPath)) {
|
|
37330
|
+
throw remoteProtocolError(`remote ~help command '${command.path}' escapes its node`);
|
|
37331
|
+
}
|
|
37332
|
+
return { ...command, path: `/${this.localPath(childCommandPath, false)}` };
|
|
37333
|
+
});
|
|
37334
|
+
const children = help.children?.map((child) => {
|
|
37335
|
+
const childPath = canonicalRemotePath(child.path, false);
|
|
37336
|
+
const parent = childPath.split("/").slice(0, -1).join("/");
|
|
37337
|
+
if (parent !== nodeChildPath) {
|
|
37338
|
+
throw remoteProtocolError(`remote ~help child '${childPath}' is not a direct descendant`);
|
|
37339
|
+
}
|
|
37340
|
+
return { ...child, path: this.localPath(childPath, false) };
|
|
37341
|
+
}).filter((child) => ctx === void 0 || visibleRemotePath(ctx, child.path, child.kind));
|
|
37342
|
+
return {
|
|
37343
|
+
node: { ...help.node, path: nodePath },
|
|
37344
|
+
cmds,
|
|
37345
|
+
...children === void 0 ? {} : { children },
|
|
37346
|
+
...help.feedback === void 0 ? {} : { feedback: help.feedback },
|
|
37347
|
+
...help.hint === void 0 ? {} : { hint: help.hint },
|
|
37348
|
+
...help.note === void 0 ? {} : { note: help.note }
|
|
37349
|
+
};
|
|
37024
37350
|
}
|
|
37025
|
-
|
|
37026
|
-
|
|
37027
|
-
|
|
37028
|
-
|
|
37029
|
-
|
|
37030
|
-
|
|
37031
|
-
|
|
37351
|
+
projectTree(tree, requestedLocalPath, ctx) {
|
|
37352
|
+
const expectedChildPath = this.childPath(requestedLocalPath);
|
|
37353
|
+
const rootChildPath = canonicalRemotePath(tree.path, expectedChildPath === "");
|
|
37354
|
+
if (rootChildPath !== expectedChildPath) {
|
|
37355
|
+
throw remoteProtocolError(
|
|
37356
|
+
`remote ~tree path '${rootChildPath}' does not match request '${expectedChildPath}'`
|
|
37357
|
+
);
|
|
37358
|
+
}
|
|
37359
|
+
const walk = (node, childPath) => {
|
|
37360
|
+
const localPath = this.localPath(childPath, childPath === "");
|
|
37361
|
+
const children = (node.children ?? []).map((child) => {
|
|
37362
|
+
const canonical = canonicalRemotePath(child.path, false);
|
|
37363
|
+
const parent = canonical.split("/").slice(0, -1).join("/");
|
|
37364
|
+
if (parent !== childPath) {
|
|
37365
|
+
throw remoteProtocolError(`remote ~tree child '${canonical}' is not a direct descendant`);
|
|
37366
|
+
}
|
|
37367
|
+
return walk(child, canonical);
|
|
37368
|
+
}).filter((child) => visibleRemotePath(ctx, child.path, child.kind));
|
|
37369
|
+
return {
|
|
37370
|
+
path: localPath,
|
|
37371
|
+
kind: node.kind,
|
|
37372
|
+
description: node.description,
|
|
37373
|
+
...children.length === 0 ? {} : { children },
|
|
37374
|
+
...node.presence === void 0 ? {} : { presence: node.presence },
|
|
37375
|
+
...node.truncated === void 0 ? {} : { truncated: node.truncated }
|
|
37376
|
+
};
|
|
37377
|
+
};
|
|
37378
|
+
return walk(tree, rootChildPath);
|
|
37379
|
+
}
|
|
37380
|
+
};
|
|
37381
|
+
async function remotePathProjectorIfMatch(registry, treePath) {
|
|
37382
|
+
const resolved = await registry.resolve(treePath).catch(() => null);
|
|
37383
|
+
if (resolved === null || resolved.node.kind !== "remote" || resolved.node.config?.kind !== "remote") {
|
|
37384
|
+
return null;
|
|
37385
|
+
}
|
|
37386
|
+
return new RemotePathProjector(resolved.node.path);
|
|
37032
37387
|
}
|
|
37033
37388
|
async function resolveRemoteSettings(state, base) {
|
|
37034
37389
|
const runtime = await new RemoteAllowlistStore(state).hosts();
|
|
@@ -37051,6 +37406,17 @@ async function remotePassthroughIfMatch(c, ctx, registry, treePath, reservedTail
|
|
|
37051
37406
|
}
|
|
37052
37407
|
const requestPath = reservedTail === null ? treePath : `${treePath}/${reservedTail}`;
|
|
37053
37408
|
const body = method === "POST" ? await c.req.text() : void 0;
|
|
37409
|
+
const outboundHeaders = new Headers(headers);
|
|
37410
|
+
const searchSettings = federatedSearchSettings(deps.remote);
|
|
37411
|
+
const rawRemaining = outboundHeaders.get(SEARCH_REMAINING_MS_HEADER);
|
|
37412
|
+
const requestedRemaining = rawRemaining !== null && /^\d+$/u.test(rawRemaining) ? Number(rawRemaining) : void 0;
|
|
37413
|
+
const remainingMs = requestedRemaining !== void 0 && Number.isSafeInteger(requestedRemaining) && requestedRemaining > 0 ? Math.min(requestedRemaining, searchSettings.totalDeadlineMs) : void 0;
|
|
37414
|
+
if (remainingMs !== void 0) {
|
|
37415
|
+
outboundHeaders.set(
|
|
37416
|
+
SEARCH_REMAINING_MS_HEADER,
|
|
37417
|
+
String(Math.max(1, remainingMs - searchSettings.perHopReturnReserveMs))
|
|
37418
|
+
);
|
|
37419
|
+
}
|
|
37054
37420
|
return await passthroughRemote({
|
|
37055
37421
|
actor: { keyId: ctx.keyId, owner: ctx.owner, traceId: ctx.traceId },
|
|
37056
37422
|
config: node.config,
|
|
@@ -37058,7 +37424,11 @@ async function remotePassthroughIfMatch(c, ctx, registry, treePath, reservedTail
|
|
|
37058
37424
|
requestPath,
|
|
37059
37425
|
method,
|
|
37060
37426
|
...body !== void 0 ? { body } : {},
|
|
37061
|
-
headers,
|
|
37427
|
+
headers: outboundHeaders,
|
|
37428
|
+
...remainingMs === void 0 ? {} : {
|
|
37429
|
+
maxResponseBodyBytes: searchSettings.maxResponseBodyBytes,
|
|
37430
|
+
signal: AbortSignal.timeout(remainingMs)
|
|
37431
|
+
},
|
|
37062
37432
|
secrets: deps.secrets,
|
|
37063
37433
|
settings: await resolveRemoteSettings(deps.state, deps.remote),
|
|
37064
37434
|
requestUrl: c.req.url
|
|
@@ -37082,15 +37452,14 @@ async function remoteTreeChildren(c, ctx, registry, treePath, deps) {
|
|
|
37082
37452
|
retryable: resp.status >= 500
|
|
37083
37453
|
});
|
|
37084
37454
|
}
|
|
37085
|
-
const
|
|
37086
|
-
|
|
37455
|
+
const rawRemoteTree = await resp.json().catch(() => null);
|
|
37456
|
+
const parsedRemoteTree = treeJsonSchema.safeParse(rawRemoteTree);
|
|
37457
|
+
if (!parsedRemoteTree.success) {
|
|
37087
37458
|
throw new TBError("unavailable", "remote ~tree returned invalid JSON", { retryable: false });
|
|
37088
37459
|
}
|
|
37089
|
-
const
|
|
37090
|
-
|
|
37091
|
-
|
|
37092
|
-
}
|
|
37093
|
-
return (remoteTree.children ?? []).map((child) => localizeRemoteEntry(resolved.node.path, remotePath, child));
|
|
37460
|
+
const projector = new RemotePathProjector(resolved.node.path);
|
|
37461
|
+
const projected = projector.projectTree(parsedRemoteTree.data, treePath, ctx);
|
|
37462
|
+
return projected.children ?? [];
|
|
37094
37463
|
}
|
|
37095
37464
|
function assertRemoteConfigAllowed(config, settings) {
|
|
37096
37465
|
if (config === null || typeof config !== "object") return;
|
|
@@ -37164,6 +37533,7 @@ async function handleRegister(c, env) {
|
|
|
37164
37533
|
}
|
|
37165
37534
|
await assertRegisterPath2(registry, ctx, body.path, "write", deps);
|
|
37166
37535
|
assertSecretRefUse(ctx.scopes, body.config);
|
|
37536
|
+
await assertMcpOAuthConfig(body.config, deps.secrets);
|
|
37167
37537
|
await assertContextConfig(body.config, deps);
|
|
37168
37538
|
await assertSkillhubConfig(body.config, deps);
|
|
37169
37539
|
await assertToolConfig(body.config, deps, ctx, body.path);
|
|
@@ -37230,7 +37600,9 @@ async function handleAuthorize(c, env) {
|
|
|
37230
37600
|
encryptionKey: encKey,
|
|
37231
37601
|
nodePath: path,
|
|
37232
37602
|
serverUrl: node.config.url,
|
|
37603
|
+
secrets: deps.secrets,
|
|
37233
37604
|
origin: deps.canonicalOrigin ?? new URL(c.req.url).origin,
|
|
37605
|
+
...node.config.oauthClient !== void 0 ? { oauthClient: node.config.oauthClient } : {},
|
|
37234
37606
|
...redirectUri !== void 0 ? { redirectUri } : {}
|
|
37235
37607
|
});
|
|
37236
37608
|
return new Response(JSON.stringify(oauthAuthorizeResponseSchema.parse(result)), {
|
|
@@ -37574,15 +37946,25 @@ function registerPublicRoutes(app, env) {
|
|
|
37574
37946
|
if (node.kind !== "mcp" || node.config?.kind !== "mcp" || node.config.auth !== "oauth") {
|
|
37575
37947
|
return renderOAuthCallbackHtml(false, "target node is not an OAuth-backed mount");
|
|
37576
37948
|
}
|
|
37949
|
+
const currentClient = node.config.oauthClient;
|
|
37950
|
+
if (payload.o === void 0 !== (currentClient === void 0) || payload.o !== void 0 && (currentClient?.clientId !== payload.o.i || currentClient.clientSecretRef !== payload.o.r)) {
|
|
37951
|
+
return renderOAuthCallbackHtml(
|
|
37952
|
+
false,
|
|
37953
|
+
"OAuth client configuration changed; restart authorization"
|
|
37954
|
+
);
|
|
37955
|
+
}
|
|
37577
37956
|
try {
|
|
37578
37957
|
await finishMcpAuthorization({
|
|
37579
37958
|
store: deps.state,
|
|
37580
37959
|
encryptionKey: encKey,
|
|
37581
37960
|
nodePath: payload.p,
|
|
37582
37961
|
serverUrl: node.config.url,
|
|
37962
|
+
secrets: deps.secrets,
|
|
37583
37963
|
origin: deps.canonicalOrigin ?? new URL(c.req.url).origin,
|
|
37584
37964
|
code,
|
|
37585
37965
|
codeVerifier: payload.v,
|
|
37966
|
+
...q.iss !== void 0 ? { authorizationResponseIssuer: q.iss } : {},
|
|
37967
|
+
...node.config.oauthClient !== void 0 ? { oauthClient: node.config.oauthClient } : {},
|
|
37586
37968
|
// 本地回调通道(CLI --local):兑换必须复用授权时的 redirect_uri。
|
|
37587
37969
|
...payload.r !== void 0 ? { redirectUri: payload.r } : {}
|
|
37588
37970
|
});
|
|
@@ -37595,7 +37977,1505 @@ function registerPublicRoutes(app, env) {
|
|
|
37595
37977
|
);
|
|
37596
37978
|
}
|
|
37597
37979
|
|
|
37980
|
+
// ../app/src/search/federatedSession.ts
|
|
37981
|
+
var FEDERATED_SEARCH_HANDLE_PREFIX = "fsc1_";
|
|
37982
|
+
var FEDERATED_SEARCH_HANDLE_RANDOM_BYTES = 24;
|
|
37983
|
+
var FEDERATED_SEARCH_HANDLE_LENGTH = 37;
|
|
37984
|
+
var FEDERATED_SEARCH_SESSION_KEY_PREFIX = "fsearch:session:v1:";
|
|
37985
|
+
var FEDERATED_SEARCH_QUOTA_KEY = "fsearch:quota:v1";
|
|
37986
|
+
var HANDLE_PATTERN = /^fsc1_[A-Za-z0-9_-]{32}$/;
|
|
37987
|
+
var MAX_SAFE_JSON_DEPTH = 64;
|
|
37988
|
+
var MAX_SAFE_JSON_NODES = 1e4;
|
|
37989
|
+
var DEFAULT_MAX_RECORD_BYTES = 1024 * 1024;
|
|
37990
|
+
var DEFAULT_MAX_BYTES_GLOBAL = 128 * 1024 * 1024;
|
|
37991
|
+
var DEFAULT_MAX_BYTES_PER_ACTOR = 32 * 1024 * 1024;
|
|
37992
|
+
var DEFAULT_MAX_BYTES_PER_SESSION = 8 * 1024 * 1024;
|
|
37993
|
+
var DEFAULT_MAX_GENERATIONS_PER_SESSION = 1024;
|
|
37994
|
+
var DEFAULT_MAX_SESSIONS_GLOBAL = 256;
|
|
37995
|
+
var DEFAULT_MAX_SESSIONS_PER_ACTOR = 64;
|
|
37996
|
+
var QUOTA_CAS_ATTEMPTS = 32;
|
|
37997
|
+
var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set([
|
|
37998
|
+
"$schema",
|
|
37999
|
+
"authorization",
|
|
38000
|
+
"baseurl",
|
|
38001
|
+
"credential",
|
|
38002
|
+
"inputschema",
|
|
38003
|
+
"outputschema",
|
|
38004
|
+
"schema",
|
|
38005
|
+
"secret",
|
|
38006
|
+
"sk",
|
|
38007
|
+
"token"
|
|
38008
|
+
]);
|
|
38009
|
+
function sessionUnavailable(message) {
|
|
38010
|
+
return new TBError("unavailable", message, { retryable: false });
|
|
38011
|
+
}
|
|
38012
|
+
function invalidSession() {
|
|
38013
|
+
return new TBError("invalid_argument", "federated search session is invalid or expired", {
|
|
38014
|
+
retryable: false
|
|
38015
|
+
});
|
|
38016
|
+
}
|
|
38017
|
+
function corruptSession() {
|
|
38018
|
+
return new TBError("internal", "federated search session state is corrupt", {
|
|
38019
|
+
retryable: false
|
|
38020
|
+
});
|
|
38021
|
+
}
|
|
38022
|
+
function isPlainObject3(value) {
|
|
38023
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
38024
|
+
const prototype = Object.getPrototypeOf(value);
|
|
38025
|
+
return prototype === Object.prototype || prototype === null;
|
|
38026
|
+
}
|
|
38027
|
+
function hasExactKeys(value, expected) {
|
|
38028
|
+
const actual = Object.keys(value).sort();
|
|
38029
|
+
const sortedExpected = [...expected].sort();
|
|
38030
|
+
return actual.length === sortedExpected.length && actual.every((key, index) => key === sortedExpected[index]);
|
|
38031
|
+
}
|
|
38032
|
+
function validOpaque(value) {
|
|
38033
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value);
|
|
38034
|
+
}
|
|
38035
|
+
function validExpiry(value) {
|
|
38036
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
38037
|
+
}
|
|
38038
|
+
function normalizedStateKey(key) {
|
|
38039
|
+
return key.replaceAll("-", "").replaceAll("_", "").toLowerCase();
|
|
38040
|
+
}
|
|
38041
|
+
function safeStateValue(value, state, depth = 0) {
|
|
38042
|
+
state.nodes++;
|
|
38043
|
+
if (state.nodes > MAX_SAFE_JSON_NODES || depth > MAX_SAFE_JSON_DEPTH) {
|
|
38044
|
+
throw new Error("federated search session value exceeds structural limits");
|
|
38045
|
+
}
|
|
38046
|
+
if (value === null || typeof value === "boolean") return value;
|
|
38047
|
+
if (typeof value === "number") {
|
|
38048
|
+
if (!Number.isFinite(value)) throw new Error("non-finite number");
|
|
38049
|
+
return value;
|
|
38050
|
+
}
|
|
38051
|
+
if (typeof value === "string") return value;
|
|
38052
|
+
if (Array.isArray(value)) {
|
|
38053
|
+
return Object.freeze(value.map((item) => safeStateValue(item, state, depth + 1)));
|
|
38054
|
+
}
|
|
38055
|
+
if (!isPlainObject3(value)) throw new Error("non-JSON value");
|
|
38056
|
+
const clone = {};
|
|
38057
|
+
for (const [key, item] of Object.entries(value)) {
|
|
38058
|
+
if (FORBIDDEN_STATE_KEYS.has(normalizedStateKey(key))) {
|
|
38059
|
+
throw new Error(`forbidden state field '${key}'`);
|
|
38060
|
+
}
|
|
38061
|
+
clone[key] = safeStateValue(item, state, depth + 1);
|
|
38062
|
+
}
|
|
38063
|
+
return Object.freeze(clone);
|
|
38064
|
+
}
|
|
38065
|
+
function parseRecord(value) {
|
|
38066
|
+
if (!isPlainObject3(value) || !hasExactKeys(value, [
|
|
38067
|
+
"actorKeyId",
|
|
38068
|
+
"excludedStatuses",
|
|
38069
|
+
"expiresAt",
|
|
38070
|
+
"federationPolicy",
|
|
38071
|
+
"generation",
|
|
38072
|
+
"nextHandle",
|
|
38073
|
+
"page",
|
|
38074
|
+
"rankingVersion",
|
|
38075
|
+
"requestDigest",
|
|
38076
|
+
"revision",
|
|
38077
|
+
"sessionId",
|
|
38078
|
+
"sourceContinuations",
|
|
38079
|
+
"topologyDigest"
|
|
38080
|
+
])) {
|
|
38081
|
+
throw corruptSession();
|
|
38082
|
+
}
|
|
38083
|
+
if (value.revision !== 0 || !validOpaque(value.sessionId) || !validOpaque(value.actorKeyId) || !validOpaque(value.requestDigest) || !validOpaque(value.rankingVersion) || !validOpaque(value.topologyDigest) || !validExpiry(value.expiresAt) || !Number.isSafeInteger(value.generation) || value.generation < 0 || value.nextHandle !== null && (typeof value.nextHandle !== "string" || !HANDLE_PATTERN.test(value.nextHandle))) {
|
|
38084
|
+
throw corruptSession();
|
|
38085
|
+
}
|
|
38086
|
+
try {
|
|
38087
|
+
const state = { nodes: 0 };
|
|
38088
|
+
return Object.freeze({
|
|
38089
|
+
actorKeyId: value.actorKeyId,
|
|
38090
|
+
excludedStatuses: safeStateValue(value.excludedStatuses, state),
|
|
38091
|
+
expiresAt: value.expiresAt,
|
|
38092
|
+
federationPolicy: safeStateValue(value.federationPolicy, state),
|
|
38093
|
+
generation: value.generation,
|
|
38094
|
+
nextHandle: value.nextHandle,
|
|
38095
|
+
page: safeStateValue(value.page, state),
|
|
38096
|
+
rankingVersion: value.rankingVersion,
|
|
38097
|
+
requestDigest: value.requestDigest,
|
|
38098
|
+
revision: 0,
|
|
38099
|
+
sessionId: value.sessionId,
|
|
38100
|
+
sourceContinuations: safeStateValue(value.sourceContinuations, state),
|
|
38101
|
+
topologyDigest: value.topologyDigest
|
|
38102
|
+
});
|
|
38103
|
+
} catch (error) {
|
|
38104
|
+
if (error instanceof TBError) throw error;
|
|
38105
|
+
throw corruptSession();
|
|
38106
|
+
}
|
|
38107
|
+
}
|
|
38108
|
+
function parseCreate(handle, input) {
|
|
38109
|
+
try {
|
|
38110
|
+
return parseRecord({
|
|
38111
|
+
...input,
|
|
38112
|
+
revision: 0,
|
|
38113
|
+
sessionId: input.sessionId ?? handle
|
|
38114
|
+
});
|
|
38115
|
+
} catch {
|
|
38116
|
+
throw new TBError("invalid_argument", "federated search session input is invalid", {
|
|
38117
|
+
retryable: false
|
|
38118
|
+
});
|
|
38119
|
+
}
|
|
38120
|
+
}
|
|
38121
|
+
function parseQuotaRecord(value) {
|
|
38122
|
+
if (!isPlainObject3(value) || !hasExactKeys(value, ["revision", "sessions"])) {
|
|
38123
|
+
throw corruptSession();
|
|
38124
|
+
}
|
|
38125
|
+
if (!Number.isSafeInteger(value.revision) || value.revision < 0 || !Array.isArray(value.sessions)) {
|
|
38126
|
+
throw corruptSession();
|
|
38127
|
+
}
|
|
38128
|
+
const sessions = value.sessions.map((item) => {
|
|
38129
|
+
if (!isPlainObject3(item) || !hasExactKeys(item, [
|
|
38130
|
+
"actorKeyId",
|
|
38131
|
+
"bytes",
|
|
38132
|
+
"expiresAt",
|
|
38133
|
+
"generation",
|
|
38134
|
+
"generationBytes",
|
|
38135
|
+
"generationId",
|
|
38136
|
+
"id"
|
|
38137
|
+
]) || !validOpaque(item.actorKeyId) || !validExpiry(item.expiresAt) || !Number.isSafeInteger(item.bytes) || item.bytes < 1 || !Number.isSafeInteger(item.generation) || item.generation < 0 || !Number.isSafeInteger(item.generationBytes) || item.generationBytes < 1 || typeof item.generationId !== "string" || !/^[0-9a-f]{64}$/u.test(item.generationId) || typeof item.id !== "string" || !/^[0-9a-f]{64}$/u.test(item.id)) {
|
|
38138
|
+
throw corruptSession();
|
|
38139
|
+
}
|
|
38140
|
+
return {
|
|
38141
|
+
actorKeyId: item.actorKeyId,
|
|
38142
|
+
bytes: item.bytes,
|
|
38143
|
+
expiresAt: item.expiresAt,
|
|
38144
|
+
generation: item.generation,
|
|
38145
|
+
generationBytes: item.generationBytes,
|
|
38146
|
+
generationId: item.generationId,
|
|
38147
|
+
id: item.id
|
|
38148
|
+
};
|
|
38149
|
+
});
|
|
38150
|
+
return { revision: value.revision, sessions };
|
|
38151
|
+
}
|
|
38152
|
+
function isFederatedSearchSessionHandle(value) {
|
|
38153
|
+
return typeof value === "string" && value.length === FEDERATED_SEARCH_HANDLE_LENGTH && HANDLE_PATTERN.test(value);
|
|
38154
|
+
}
|
|
38155
|
+
async function federatedSearchSessionStateKey(handle) {
|
|
38156
|
+
if (!isFederatedSearchSessionHandle(handle)) throw invalidSession();
|
|
38157
|
+
return FEDERATED_SEARCH_SESSION_KEY_PREFIX + await sha256Hex(handle);
|
|
38158
|
+
}
|
|
38159
|
+
var FederatedSearchSessionStore = class {
|
|
38160
|
+
constructor(state, opts = {}) {
|
|
38161
|
+
this.state = state;
|
|
38162
|
+
if (state.compareAndSwap === void 0) {
|
|
38163
|
+
throw sessionUnavailable("federated search sessions require StateStore.compareAndSwap");
|
|
38164
|
+
}
|
|
38165
|
+
this.cas = state.compareAndSwap.bind(state);
|
|
38166
|
+
this.maxBytesGlobal = opts.maxBytesGlobal ?? DEFAULT_MAX_BYTES_GLOBAL;
|
|
38167
|
+
this.maxBytesPerActor = opts.maxBytesPerActor ?? DEFAULT_MAX_BYTES_PER_ACTOR;
|
|
38168
|
+
this.maxBytesPerSession = opts.maxBytesPerSession ?? DEFAULT_MAX_BYTES_PER_SESSION;
|
|
38169
|
+
this.maxGenerationsPerSession = opts.maxGenerationsPerSession ?? DEFAULT_MAX_GENERATIONS_PER_SESSION;
|
|
38170
|
+
this.maxRecordBytes = opts.maxRecordBytes ?? DEFAULT_MAX_RECORD_BYTES;
|
|
38171
|
+
this.maxSessionsGlobal = opts.maxSessionsGlobal ?? DEFAULT_MAX_SESSIONS_GLOBAL;
|
|
38172
|
+
this.maxSessionsPerActor = opts.maxSessionsPerActor ?? DEFAULT_MAX_SESSIONS_PER_ACTOR;
|
|
38173
|
+
this.now = opts.now ?? (() => Date.now());
|
|
38174
|
+
this.randomBytes = opts.randomBytes ?? ((length) => crypto.getRandomValues(new Uint8Array(length)));
|
|
38175
|
+
}
|
|
38176
|
+
state;
|
|
38177
|
+
cas;
|
|
38178
|
+
now;
|
|
38179
|
+
randomBytes;
|
|
38180
|
+
maxBytesGlobal;
|
|
38181
|
+
maxBytesPerActor;
|
|
38182
|
+
maxBytesPerSession;
|
|
38183
|
+
maxGenerationsPerSession;
|
|
38184
|
+
maxRecordBytes;
|
|
38185
|
+
maxSessionsGlobal;
|
|
38186
|
+
maxSessionsPerActor;
|
|
38187
|
+
async cleanupExpiredSessionRows() {
|
|
38188
|
+
const page = await this.state.list(FEDERATED_SEARCH_SESSION_KEY_PREFIX, { limit: 32 });
|
|
38189
|
+
for (const item of page.items) {
|
|
38190
|
+
let record2;
|
|
38191
|
+
try {
|
|
38192
|
+
record2 = parseRecord(item.value);
|
|
38193
|
+
} catch {
|
|
38194
|
+
continue;
|
|
38195
|
+
}
|
|
38196
|
+
if (Date.parse(record2.expiresAt) <= this.now()) {
|
|
38197
|
+
await this.state.delete(item.key).catch(() => {
|
|
38198
|
+
});
|
|
38199
|
+
}
|
|
38200
|
+
}
|
|
38201
|
+
}
|
|
38202
|
+
async reserveQuota(handle, record2, recordBytes) {
|
|
38203
|
+
const id = await sha256Hex(record2.sessionId);
|
|
38204
|
+
const generationId = await sha256Hex(handle);
|
|
38205
|
+
for (let attempt2 = 0; attempt2 < QUOTA_CAS_ATTEMPTS; attempt2++) {
|
|
38206
|
+
const raw = await this.state.get(FEDERATED_SEARCH_QUOTA_KEY);
|
|
38207
|
+
const current = raw === null ? { revision: 0, sessions: [] } : parseQuotaRecord(raw);
|
|
38208
|
+
const active = current.sessions.filter((entry) => Date.parse(entry.expiresAt) > this.now());
|
|
38209
|
+
const existing = active.find((entry) => entry.id === id);
|
|
38210
|
+
let nextEntry;
|
|
38211
|
+
if (existing !== void 0) {
|
|
38212
|
+
if (existing.actorKeyId !== record2.actorKeyId || existing.expiresAt !== record2.expiresAt) throw invalidSession();
|
|
38213
|
+
if (record2.generation === existing.generation) {
|
|
38214
|
+
if (existing.generationId !== generationId || existing.generationBytes !== recordBytes) throw invalidSession();
|
|
38215
|
+
return;
|
|
38216
|
+
}
|
|
38217
|
+
if (record2.generation !== existing.generation + 1 || generationId === existing.generationId) throw invalidSession();
|
|
38218
|
+
nextEntry = {
|
|
38219
|
+
...existing,
|
|
38220
|
+
bytes: existing.bytes + recordBytes,
|
|
38221
|
+
generation: record2.generation,
|
|
38222
|
+
generationBytes: recordBytes,
|
|
38223
|
+
generationId
|
|
38224
|
+
};
|
|
38225
|
+
} else {
|
|
38226
|
+
if (record2.generation !== 0) throw invalidSession();
|
|
38227
|
+
nextEntry = {
|
|
38228
|
+
actorKeyId: record2.actorKeyId,
|
|
38229
|
+
bytes: recordBytes,
|
|
38230
|
+
expiresAt: record2.expiresAt,
|
|
38231
|
+
generation: 0,
|
|
38232
|
+
generationBytes: recordBytes,
|
|
38233
|
+
generationId,
|
|
38234
|
+
id
|
|
38235
|
+
};
|
|
38236
|
+
}
|
|
38237
|
+
const actorCount = active.filter((entry) => entry.actorKeyId === record2.actorKeyId).length;
|
|
38238
|
+
const globalBytes = active.reduce((total, entry) => total + entry.bytes, 0);
|
|
38239
|
+
const actorBytes = active.filter((entry) => entry.actorKeyId === record2.actorKeyId).reduce((total, entry) => total + entry.bytes, 0);
|
|
38240
|
+
const priorBytes = existing?.bytes ?? 0;
|
|
38241
|
+
if (record2.generation >= this.maxGenerationsPerSession || nextEntry.bytes > this.maxBytesPerSession) {
|
|
38242
|
+
throw new TBError("rate_limited", "federated search session chain quota exceeded", {
|
|
38243
|
+
retryable: false
|
|
38244
|
+
});
|
|
38245
|
+
}
|
|
38246
|
+
if (existing === void 0 && active.length >= this.maxSessionsGlobal || existing === void 0 && actorCount >= this.maxSessionsPerActor || globalBytes - priorBytes + nextEntry.bytes > this.maxBytesGlobal || actorBytes - priorBytes + nextEntry.bytes > this.maxBytesPerActor) {
|
|
38247
|
+
throw new TBError("rate_limited", "federated search session quota exceeded", {
|
|
38248
|
+
retryable: true
|
|
38249
|
+
});
|
|
38250
|
+
}
|
|
38251
|
+
const next = {
|
|
38252
|
+
revision: raw === null ? 0 : current.revision + 1,
|
|
38253
|
+
sessions: [
|
|
38254
|
+
...active.filter((entry) => entry.id !== id),
|
|
38255
|
+
nextEntry
|
|
38256
|
+
]
|
|
38257
|
+
};
|
|
38258
|
+
if (await this.cas(
|
|
38259
|
+
FEDERATED_SEARCH_QUOTA_KEY,
|
|
38260
|
+
raw === null ? null : current.revision,
|
|
38261
|
+
next
|
|
38262
|
+
)) return;
|
|
38263
|
+
}
|
|
38264
|
+
throw new TBError("unavailable", "federated search session quota is busy", {
|
|
38265
|
+
retryable: true
|
|
38266
|
+
});
|
|
38267
|
+
}
|
|
38268
|
+
issueHandle() {
|
|
38269
|
+
const bytes = this.randomBytes(FEDERATED_SEARCH_HANDLE_RANDOM_BYTES);
|
|
38270
|
+
if (!(bytes instanceof Uint8Array) || bytes.length !== FEDERATED_SEARCH_HANDLE_RANDOM_BYTES) {
|
|
38271
|
+
throw sessionUnavailable("federated search session randomness is unavailable");
|
|
38272
|
+
}
|
|
38273
|
+
const handle = FEDERATED_SEARCH_HANDLE_PREFIX + base64urlEncode(bytes);
|
|
38274
|
+
if (!isFederatedSearchSessionHandle(handle)) {
|
|
38275
|
+
throw sessionUnavailable("federated search session randomness is invalid");
|
|
38276
|
+
}
|
|
38277
|
+
return handle;
|
|
38278
|
+
}
|
|
38279
|
+
async create(handle, input) {
|
|
38280
|
+
const key = await federatedSearchSessionStateKey(handle);
|
|
38281
|
+
const record2 = parseCreate(handle, input);
|
|
38282
|
+
if (Date.parse(record2.expiresAt) <= this.now()) throw invalidSession();
|
|
38283
|
+
const recordBytes = new TextEncoder().encode(JSON.stringify(record2)).length;
|
|
38284
|
+
if (recordBytes > this.maxRecordBytes) {
|
|
38285
|
+
throw new TBError("rate_limited", "federated search session record is too large", {
|
|
38286
|
+
retryable: false
|
|
38287
|
+
});
|
|
38288
|
+
}
|
|
38289
|
+
if (await this.state.get(key) !== null) return await this.read(handle, input);
|
|
38290
|
+
if (record2.generation === 0) await this.cleanupExpiredSessionRows();
|
|
38291
|
+
await this.reserveQuota(handle, record2, recordBytes);
|
|
38292
|
+
if (await this.cas(key, null, record2)) {
|
|
38293
|
+
return record2;
|
|
38294
|
+
}
|
|
38295
|
+
return await this.read(handle, input);
|
|
38296
|
+
}
|
|
38297
|
+
async createNew(input) {
|
|
38298
|
+
const handle = this.issueHandle();
|
|
38299
|
+
return { handle, record: await this.create(handle, input) };
|
|
38300
|
+
}
|
|
38301
|
+
async read(handle, binding) {
|
|
38302
|
+
if (!validOpaque(binding.actorKeyId) || !validOpaque(binding.requestDigest)) {
|
|
38303
|
+
throw invalidSession();
|
|
38304
|
+
}
|
|
38305
|
+
const raw = await this.state.get(await federatedSearchSessionStateKey(handle));
|
|
38306
|
+
if (raw === null) throw invalidSession();
|
|
38307
|
+
const record2 = parseRecord(raw);
|
|
38308
|
+
if (Date.parse(record2.expiresAt) <= this.now()) {
|
|
38309
|
+
await this.state.delete(await federatedSearchSessionStateKey(handle)).catch(() => {
|
|
38310
|
+
});
|
|
38311
|
+
throw invalidSession();
|
|
38312
|
+
}
|
|
38313
|
+
if (record2.actorKeyId !== binding.actorKeyId || record2.requestDigest !== binding.requestDigest) {
|
|
38314
|
+
throw invalidSession();
|
|
38315
|
+
}
|
|
38316
|
+
return record2;
|
|
38317
|
+
}
|
|
38318
|
+
};
|
|
38319
|
+
|
|
38320
|
+
// ../app/src/search/localSearch.ts
|
|
38321
|
+
function normalizedEffect(effect) {
|
|
38322
|
+
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
38323
|
+
}
|
|
38324
|
+
function projectSearchTool(tool, detail) {
|
|
38325
|
+
if (detail === "full") return tool;
|
|
38326
|
+
const compact = { ...tool };
|
|
38327
|
+
delete compact.inputSchema;
|
|
38328
|
+
delete compact.outputSchema;
|
|
38329
|
+
if (compact.description !== void 0) {
|
|
38330
|
+
const encoder = new TextEncoder();
|
|
38331
|
+
if (encoder.encode(compact.description).length > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) {
|
|
38332
|
+
let description = "";
|
|
38333
|
+
let bytes = 0;
|
|
38334
|
+
for (const char of compact.description) {
|
|
38335
|
+
const size = encoder.encode(char).length;
|
|
38336
|
+
if (bytes + size > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) break;
|
|
38337
|
+
description += char;
|
|
38338
|
+
bytes += size;
|
|
38339
|
+
}
|
|
38340
|
+
compact.description = description;
|
|
38341
|
+
}
|
|
38342
|
+
}
|
|
38343
|
+
return compact;
|
|
38344
|
+
}
|
|
38345
|
+
async function executeLocalSearch(deps, request) {
|
|
38346
|
+
const { ctx, search, searchSync, state } = deps;
|
|
38347
|
+
const query = request.query;
|
|
38348
|
+
const wireOpts = request.opts ?? {};
|
|
38349
|
+
const detail = wireOpts.detail ?? "compact";
|
|
38350
|
+
const matching = wireOpts.matching ?? "best";
|
|
38351
|
+
const mode = wireOpts.mode ?? "keyword";
|
|
38352
|
+
const limit = normalizeToolSearchLimit(wireOpts.limit);
|
|
38353
|
+
const constraints = normalizeToolSearchOptions({
|
|
38354
|
+
...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
|
|
38355
|
+
matching,
|
|
38356
|
+
...wireOpts.minCoverage === void 0 ? {} : { minCoverage: wireOpts.minCoverage },
|
|
38357
|
+
...wireOpts.pathPrefix === void 0 ? {} : { pathPrefix: wireOpts.pathPrefix }
|
|
38358
|
+
});
|
|
38359
|
+
const indexOptions = { mode, ...constraints };
|
|
38360
|
+
await searchSync?.ensureReady();
|
|
38361
|
+
const registry = new NodeRegistryStore(state);
|
|
38362
|
+
const selected = [];
|
|
38363
|
+
const workLimit = Math.min(
|
|
38364
|
+
TOOL_SEARCH_WORK_LIMIT,
|
|
38365
|
+
Math.max(TOOL_SEARCH_BATCH_LIMIT, limit * 2)
|
|
38366
|
+
);
|
|
38367
|
+
let scanCursor = wireOpts.cursor;
|
|
38368
|
+
let responseCursor;
|
|
38369
|
+
let responseCandidate;
|
|
38370
|
+
let visibleCoverage;
|
|
38371
|
+
let scanned = 0;
|
|
38372
|
+
let stopped = false;
|
|
38373
|
+
while (!stopped && selected.length < limit && scanned < workLimit) {
|
|
38374
|
+
const batchLimit = Math.min(TOOL_SEARCH_BATCH_LIMIT, workLimit - scanned);
|
|
38375
|
+
const page = await search.search(query, {
|
|
38376
|
+
...indexOptions,
|
|
38377
|
+
limit: batchLimit,
|
|
38378
|
+
...scanCursor === void 0 ? {} : { cursor: scanCursor }
|
|
38379
|
+
});
|
|
38380
|
+
if (page.items.length === 0) {
|
|
38381
|
+
responseCursor = page.cursor;
|
|
38382
|
+
break;
|
|
38383
|
+
}
|
|
38384
|
+
const nodes = await registry.getMany(page.items.map((candidate) => candidate.path));
|
|
38385
|
+
let lastCandidateInVisibleBand;
|
|
38386
|
+
for (const [index, candidate] of page.items.entries()) {
|
|
38387
|
+
if (matching === "best" && visibleCoverage !== void 0 && candidate.coverage !== visibleCoverage) {
|
|
38388
|
+
responseCandidate = lastCandidateInVisibleBand;
|
|
38389
|
+
responseCursor = responseCandidate === void 0 ? scanCursor : void 0;
|
|
38390
|
+
stopped = true;
|
|
38391
|
+
break;
|
|
38392
|
+
}
|
|
38393
|
+
scanned++;
|
|
38394
|
+
lastCandidateInVisibleBand = candidate;
|
|
38395
|
+
if (!check(ctx, candidate.path, "read").allow || !check(ctx, candidate.path, "call").allow) {
|
|
38396
|
+
continue;
|
|
38397
|
+
}
|
|
38398
|
+
const node = nodes.get(candidate.path);
|
|
38399
|
+
if (node === void 0 || node.kind !== "mcp" && node.kind !== "http" && node.kind !== "tool" || node.config?.kind !== node.kind || virtualizeTools(node.virtualize, [{ name: candidate.name }]).exposed.length === 0) {
|
|
38400
|
+
continue;
|
|
38401
|
+
}
|
|
38402
|
+
visibleCoverage ??= candidate.coverage;
|
|
38403
|
+
selected.push({ candidate, node });
|
|
38404
|
+
if (selected.length === limit) {
|
|
38405
|
+
const hasUnscanned = index < page.items.length - 1 || page.cursor !== void 0;
|
|
38406
|
+
responseCandidate = hasUnscanned ? candidate : void 0;
|
|
38407
|
+
responseCursor = void 0;
|
|
38408
|
+
stopped = true;
|
|
38409
|
+
break;
|
|
38410
|
+
}
|
|
38411
|
+
}
|
|
38412
|
+
if (stopped) break;
|
|
38413
|
+
responseCursor = page.cursor;
|
|
38414
|
+
if (page.cursor === void 0) break;
|
|
38415
|
+
scanCursor = page.cursor;
|
|
38416
|
+
}
|
|
38417
|
+
const canonicalTools = await canonicalSearchTools(state, selected.map((item) => item.node));
|
|
38418
|
+
const requestedEffects = constraints.effects === void 0 ? void 0 : new Set(constraints.effects);
|
|
38419
|
+
const items = [];
|
|
38420
|
+
let pageBytes = 0;
|
|
38421
|
+
for (const [index, selectedItem] of selected.entries()) {
|
|
38422
|
+
const raw = canonicalTools.get(selectedItem.candidate.path)?.find((tool2) => tool2.name === selectedItem.candidate.name);
|
|
38423
|
+
if (raw === void 0) continue;
|
|
38424
|
+
const tool = virtualizeTools(selectedItem.node.virtualize, [raw]).exposed[0];
|
|
38425
|
+
if (tool === void 0) continue;
|
|
38426
|
+
if (requestedEffects !== void 0 && !requestedEffects.has(normalizedEffect(tool.effect))) {
|
|
38427
|
+
continue;
|
|
38428
|
+
}
|
|
38429
|
+
const item = {
|
|
38430
|
+
path: selectedItem.candidate.path,
|
|
38431
|
+
relevance: {
|
|
38432
|
+
coverage: selectedItem.candidate.coverage,
|
|
38433
|
+
matchedTermCount: selectedItem.candidate.matchedTermCount,
|
|
38434
|
+
rankingVersion: "keyword-v2",
|
|
38435
|
+
totalTermCount: selectedItem.candidate.totalTermCount
|
|
38436
|
+
},
|
|
38437
|
+
tool: projectSearchTool(tool, detail)
|
|
38438
|
+
};
|
|
38439
|
+
const itemBytes = new TextEncoder().encode(JSON.stringify(item)).length;
|
|
38440
|
+
if (pageBytes + itemBytes > TOOL_SEARCH_PAGE_BYTES) {
|
|
38441
|
+
if (items.length === 0) {
|
|
38442
|
+
throw new TBError("internal", "\u5DE5\u5177\u641C\u7D22\u9875\u9762\u5B57\u8282\u9884\u7B97\u65E0\u6CD5\u5BB9\u7EB3\u9996\u4E2A\u7ED3\u679C");
|
|
38443
|
+
}
|
|
38444
|
+
responseCandidate = selected[index - 1]?.candidate;
|
|
38445
|
+
responseCursor = void 0;
|
|
38446
|
+
break;
|
|
38447
|
+
}
|
|
38448
|
+
items.push(item);
|
|
38449
|
+
pageBytes += itemBytes;
|
|
38450
|
+
}
|
|
38451
|
+
if (responseCandidate !== void 0) {
|
|
38452
|
+
responseCursor = await search.cursorFor(query, responseCandidate, mode);
|
|
38453
|
+
}
|
|
38454
|
+
if (items.length === 0) responseCursor = void 0;
|
|
38455
|
+
return responseCursor === void 0 ? { items } : { items, cursor: responseCursor };
|
|
38456
|
+
}
|
|
38457
|
+
|
|
38458
|
+
// ../app/src/search/federatedSearch.ts
|
|
38459
|
+
var HEADER_REMAINING_MS = "x-tb-search-remaining-ms";
|
|
38460
|
+
var HEADER_SESSION_TTL_MS = "x-tb-search-session-ttl-ms";
|
|
38461
|
+
var HEADER_SOURCE_BUDGET = "x-tb-search-source-budget";
|
|
38462
|
+
var HEADER_VALIDATE_SNAPSHOT = "x-tb-search-validate-snapshot";
|
|
38463
|
+
var HEADER_VIA = "x-tb-via";
|
|
38464
|
+
var HEADER_WANT_SNAPSHOT = "x-tb-search-want-snapshot";
|
|
38465
|
+
function clampHeader(value, local) {
|
|
38466
|
+
if (value === null || !/^\d+$/u.test(value)) return local;
|
|
38467
|
+
const parsed = Number(value);
|
|
38468
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? Math.min(parsed, local) : local;
|
|
38469
|
+
}
|
|
38470
|
+
function isPrefix(prefix, path) {
|
|
38471
|
+
return prefix === "" || prefix === path || path.startsWith(`${prefix}/`);
|
|
38472
|
+
}
|
|
38473
|
+
function controlledStatus(error) {
|
|
38474
|
+
if (error instanceof TBError) {
|
|
38475
|
+
if (error.message.includes("\u73AF")) return "cycle";
|
|
38476
|
+
if (error.message.includes("\u8DF3\u6570")) return "hop_limit";
|
|
38477
|
+
if (error.message.includes("aborted")) return "timed_out";
|
|
38478
|
+
if (error.message.includes("work budget")) return "budget_exhausted";
|
|
38479
|
+
}
|
|
38480
|
+
return "unavailable";
|
|
38481
|
+
}
|
|
38482
|
+
function retryableContinuationError() {
|
|
38483
|
+
return new TBError("unavailable", "federated search continuation source unavailable", {
|
|
38484
|
+
retryable: true
|
|
38485
|
+
});
|
|
38486
|
+
}
|
|
38487
|
+
function invalidContinuation() {
|
|
38488
|
+
return new TBError("invalid_argument", "federated search cursor no longer matches topology", {
|
|
38489
|
+
retryable: false
|
|
38490
|
+
});
|
|
38491
|
+
}
|
|
38492
|
+
function continuationSourceError(error) {
|
|
38493
|
+
if (error instanceof TBError) {
|
|
38494
|
+
if (error.code === "invalid_argument") return error;
|
|
38495
|
+
if (error.message.includes("source work budget exhausted")) return invalidContinuation();
|
|
38496
|
+
}
|
|
38497
|
+
return retryableContinuationError();
|
|
38498
|
+
}
|
|
38499
|
+
function compactItem(item) {
|
|
38500
|
+
const tool = { ...item.tool };
|
|
38501
|
+
delete tool.inputSchema;
|
|
38502
|
+
delete tool.outputSchema;
|
|
38503
|
+
if (tool.description !== void 0) {
|
|
38504
|
+
const encoder = new TextEncoder();
|
|
38505
|
+
if (encoder.encode(tool.description).length > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) {
|
|
38506
|
+
let description = "";
|
|
38507
|
+
let bytes = 0;
|
|
38508
|
+
for (const char of tool.description) {
|
|
38509
|
+
const size = encoder.encode(char).length;
|
|
38510
|
+
if (bytes + size > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) break;
|
|
38511
|
+
description += char;
|
|
38512
|
+
bytes += size;
|
|
38513
|
+
}
|
|
38514
|
+
tool.description = description;
|
|
38515
|
+
}
|
|
38516
|
+
}
|
|
38517
|
+
return { ...item, tool };
|
|
38518
|
+
}
|
|
38519
|
+
function normalizedEffect2(effect) {
|
|
38520
|
+
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
38521
|
+
}
|
|
38522
|
+
function requestConstraints(request) {
|
|
38523
|
+
return normalizeToolSearchOptions({
|
|
38524
|
+
...request.opts?.effects === void 0 ? {} : { effects: request.opts.effects },
|
|
38525
|
+
matching: request.opts?.matching ?? "best",
|
|
38526
|
+
...request.opts?.minCoverage === void 0 ? {} : { minCoverage: request.opts.minCoverage },
|
|
38527
|
+
...request.opts?.pathPrefix === void 0 ? {} : { pathPrefix: request.opts.pathPrefix }
|
|
38528
|
+
});
|
|
38529
|
+
}
|
|
38530
|
+
function assertHardResultConstraints(item, constraints) {
|
|
38531
|
+
const effects = constraints.effects === void 0 ? void 0 : new Set(constraints.effects);
|
|
38532
|
+
if (constraints.pathPrefix !== void 0 && !isPrefix(constraints.pathPrefix, item.path) || constraints.minCoverage !== void 0 && item.relevance.coverage < constraints.minCoverage || effects !== void 0 && !effects.has(normalizedEffect2(item.tool.effect))) {
|
|
38533
|
+
throw new TBError("unavailable", "remote search violated a hard result constraint", {
|
|
38534
|
+
retryable: false
|
|
38535
|
+
});
|
|
38536
|
+
}
|
|
38537
|
+
}
|
|
38538
|
+
function sourceStatusKey(value) {
|
|
38539
|
+
return `${value.path}\0${value.status}`;
|
|
38540
|
+
}
|
|
38541
|
+
function mergeStatuses(...groups) {
|
|
38542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38543
|
+
const out = [];
|
|
38544
|
+
for (const item of groups.flat()) {
|
|
38545
|
+
const key = sourceStatusKey(item);
|
|
38546
|
+
if (seen.has(key)) continue;
|
|
38547
|
+
seen.add(key);
|
|
38548
|
+
out.push(item);
|
|
38549
|
+
}
|
|
38550
|
+
return out.sort((left, right) => left.path.localeCompare(right.path) || left.status.localeCompare(right.status));
|
|
38551
|
+
}
|
|
38552
|
+
function responseHasMore(sources) {
|
|
38553
|
+
return sources.some((source) => source.pending !== void 0 || !source.done);
|
|
38554
|
+
}
|
|
38555
|
+
function serializableSources(sources) {
|
|
38556
|
+
return sources.map((source) => ({
|
|
38557
|
+
balanceCount: source.balanceCount,
|
|
38558
|
+
...source.balanceCoverage === void 0 ? {} : { balanceCoverage: source.balanceCoverage },
|
|
38559
|
+
budget: source.budget,
|
|
38560
|
+
done: source.done,
|
|
38561
|
+
...source.excluded === void 0 ? {} : { excluded: source.excluded },
|
|
38562
|
+
kind: source.kind,
|
|
38563
|
+
path: source.path,
|
|
38564
|
+
...source.cursor === void 0 ? {} : { cursor: source.cursor },
|
|
38565
|
+
...source.nextCursor === void 0 ? {} : { nextCursor: source.nextCursor },
|
|
38566
|
+
...source.pending === void 0 ? {} : { pending: compactItem(source.pending) },
|
|
38567
|
+
...source.validationCursor === void 0 ? {} : { validationCursor: source.validationCursor }
|
|
38568
|
+
}));
|
|
38569
|
+
}
|
|
38570
|
+
function canonicalJson(value) {
|
|
38571
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
38572
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
38573
|
+
const object2 = value;
|
|
38574
|
+
return `{${Object.keys(object2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object2[key])}`).join(",")}}`;
|
|
38575
|
+
}
|
|
38576
|
+
function sameJson(left, right) {
|
|
38577
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
38578
|
+
}
|
|
38579
|
+
function assertNextRecord(current, next, topology) {
|
|
38580
|
+
if (next.topologyDigest !== topology || next.rankingVersion !== TOOL_SEARCH_RANKING_VERSION || next.sessionId !== current.sessionId || next.generation !== current.generation + 1 || next.expiresAt !== current.expiresAt || !sameJson(next.federationPolicy, current.federationPolicy)) {
|
|
38581
|
+
throw invalidContinuation();
|
|
38582
|
+
}
|
|
38583
|
+
}
|
|
38584
|
+
function assertCachedPage(computed, cached) {
|
|
38585
|
+
if (!sameJson(computed.page, cached.page) || !sameJson(computed.excluded, cached.excludedStatuses) || !sameJson(serializableSources(computed.sources), cached.sourceContinuations)) {
|
|
38586
|
+
throw invalidContinuation();
|
|
38587
|
+
}
|
|
38588
|
+
}
|
|
38589
|
+
function requestForDigest(request, limit) {
|
|
38590
|
+
const opts = request.opts ?? {};
|
|
38591
|
+
return {
|
|
38592
|
+
query: request.query,
|
|
38593
|
+
opts: {
|
|
38594
|
+
effects: opts.effects ?? null,
|
|
38595
|
+
federation: "recursive",
|
|
38596
|
+
limit,
|
|
38597
|
+
matching: opts.matching ?? "best",
|
|
38598
|
+
minCoverage: opts.minCoverage ?? null,
|
|
38599
|
+
mode: opts.mode ?? "keyword",
|
|
38600
|
+
pathPrefix: opts.pathPrefix ?? null
|
|
38601
|
+
}
|
|
38602
|
+
};
|
|
38603
|
+
}
|
|
38604
|
+
async function topologyDigest(nodes, includeLocal, localRevision, remotePolicy) {
|
|
38605
|
+
return await sha256Hex(canonicalJson({
|
|
38606
|
+
local: includeLocal,
|
|
38607
|
+
localRevision,
|
|
38608
|
+
nodes: nodes.map((node) => ({
|
|
38609
|
+
kind: node.kind,
|
|
38610
|
+
path: node.path,
|
|
38611
|
+
remote: node.config?.kind === "remote" ? { baseUrl: node.config.baseUrl, skRef: node.config.skRef ?? null } : null,
|
|
38612
|
+
updatedAt: node.updatedAt ?? "",
|
|
38613
|
+
virtualize: node.virtualize ?? null
|
|
38614
|
+
})),
|
|
38615
|
+
remotePolicy
|
|
38616
|
+
}));
|
|
38617
|
+
}
|
|
38618
|
+
function directRemoteNodes(nodes) {
|
|
38619
|
+
const remotes = nodes.filter((node) => node.kind === "remote" && node.config?.kind === "remote").sort((left, right) => left.path.localeCompare(right.path));
|
|
38620
|
+
return remotes.filter((node) => !remotes.some((parent) => parent.path !== node.path && isPrefix(parent.path, node.path)));
|
|
38621
|
+
}
|
|
38622
|
+
function childPrefixFor(mount, pathPrefix) {
|
|
38623
|
+
if (pathPrefix === void 0) return { eligible: true };
|
|
38624
|
+
if (isPrefix(mount, pathPrefix)) {
|
|
38625
|
+
const child = new RemotePathProjector(mount).childPath(pathPrefix);
|
|
38626
|
+
return { eligible: true, ...child === "" ? {} : { pathPrefix: child } };
|
|
38627
|
+
}
|
|
38628
|
+
return { eligible: isPrefix(pathPrefix, mount) };
|
|
38629
|
+
}
|
|
38630
|
+
function localEligible(remotes, pathPrefix) {
|
|
38631
|
+
return pathPrefix === void 0 || !remotes.some((node) => isPrefix(node.path, pathPrefix));
|
|
38632
|
+
}
|
|
38633
|
+
async function mapLimit(values, concurrency, fn) {
|
|
38634
|
+
const results = new Array(values.length);
|
|
38635
|
+
let next = 0;
|
|
38636
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
38637
|
+
while (next < values.length) {
|
|
38638
|
+
const index = next++;
|
|
38639
|
+
results[index] = await fn(values[index]);
|
|
38640
|
+
}
|
|
38641
|
+
});
|
|
38642
|
+
await Promise.all(workers);
|
|
38643
|
+
return results;
|
|
38644
|
+
}
|
|
38645
|
+
function deadlineSignal(deadline) {
|
|
38646
|
+
return AbortSignal.timeout(Math.max(1, Math.floor(deadline - Date.now())));
|
|
38647
|
+
}
|
|
38648
|
+
function federatedHeaders(inbound, remainingMs, sourceBudget, sessionTtlMs) {
|
|
38649
|
+
const headers = new Headers();
|
|
38650
|
+
headers.set("accept", "application/json");
|
|
38651
|
+
headers.set("content-type", "application/json");
|
|
38652
|
+
const via = inbound.get(HEADER_VIA);
|
|
38653
|
+
if (via !== null) headers.set(HEADER_VIA, via);
|
|
38654
|
+
headers.set(HEADER_REMAINING_MS, String(Math.max(1, Math.floor(remainingMs))));
|
|
38655
|
+
headers.set(HEADER_SESSION_TTL_MS, String(Math.max(1, Math.floor(sessionTtlMs))));
|
|
38656
|
+
headers.set(HEADER_SOURCE_BUDGET, String(Math.max(1, Math.floor(sourceBudget))));
|
|
38657
|
+
headers.set(HEADER_WANT_SNAPSHOT, "1");
|
|
38658
|
+
return headers;
|
|
38659
|
+
}
|
|
38660
|
+
function projectChildPage(mount, page, totalTermCount, ctx, request) {
|
|
38661
|
+
const projector = new RemotePathProjector(mount);
|
|
38662
|
+
const constraints = requestConstraints(request);
|
|
38663
|
+
const items = page.items.map((item) => {
|
|
38664
|
+
if (item.relevance.rankingVersion !== TOOL_SEARCH_RANKING_VERSION || item.relevance.totalTermCount !== totalTermCount || item.relevance.matchedTermCount < 1 || item.relevance.matchedTermCount > totalTermCount || item.relevance.coverage !== item.relevance.matchedTermCount / totalTermCount) {
|
|
38665
|
+
throw new TBError("unavailable", "remote search returned incompatible relevance evidence", {
|
|
38666
|
+
retryable: false
|
|
38667
|
+
});
|
|
38668
|
+
}
|
|
38669
|
+
const path = projector.localPath(item.path, false);
|
|
38670
|
+
const sourcePath = projector.localPath(item.source?.path ?? "", true);
|
|
38671
|
+
if (item.tool.name.includes("/")) {
|
|
38672
|
+
throw new TBError("unavailable", "remote search returned an unsafe tool name", {
|
|
38673
|
+
retryable: false
|
|
38674
|
+
});
|
|
38675
|
+
}
|
|
38676
|
+
try {
|
|
38677
|
+
if (canonicalRemotePath(item.tool.name, false) !== item.tool.name.toLowerCase()) {
|
|
38678
|
+
throw new Error("non-canonical");
|
|
38679
|
+
}
|
|
38680
|
+
} catch {
|
|
38681
|
+
throw new TBError("unavailable", "remote search returned an unsafe tool name", {
|
|
38682
|
+
retryable: false
|
|
38683
|
+
});
|
|
38684
|
+
}
|
|
38685
|
+
if (!isPrefix(sourcePath, path)) {
|
|
38686
|
+
throw new TBError("unavailable", "remote search source does not own its result", {
|
|
38687
|
+
retryable: false
|
|
38688
|
+
});
|
|
38689
|
+
}
|
|
38690
|
+
const projected = compactItem({ ...item, path, source: { path: sourcePath } });
|
|
38691
|
+
assertHardResultConstraints(projected, constraints);
|
|
38692
|
+
return projected;
|
|
38693
|
+
}).filter((item) => check(ctx, item.path, "read").allow && check(ctx, item.path, "call").allow);
|
|
38694
|
+
const projectedSources = page.sources?.map((source) => ({
|
|
38695
|
+
path: projector.localPath(source.path, true),
|
|
38696
|
+
status: source.status
|
|
38697
|
+
}));
|
|
38698
|
+
const sourceStatuses = /* @__PURE__ */ new Map();
|
|
38699
|
+
for (const source of projectedSources ?? []) {
|
|
38700
|
+
const previous = sourceStatuses.get(source.path);
|
|
38701
|
+
if (previous !== void 0 && previous !== source.status) {
|
|
38702
|
+
throw new TBError("unavailable", "remote search returned conflicting source statuses", {
|
|
38703
|
+
retryable: false
|
|
38704
|
+
});
|
|
38705
|
+
}
|
|
38706
|
+
sourceStatuses.set(source.path, source.status);
|
|
38707
|
+
}
|
|
38708
|
+
const hasFailure = [...sourceStatuses.values()].some((status) => status !== "ok");
|
|
38709
|
+
if (page.partial === true !== hasFailure || page.cursor !== void 0 && !isFederatedSearchSessionHandle(page.cursor) || page.snapshot !== void 0 && !isFederatedSearchSessionHandle(page.snapshot)) {
|
|
38710
|
+
throw new TBError("unavailable", "remote search returned inconsistent page metadata", {
|
|
38711
|
+
retryable: false
|
|
38712
|
+
});
|
|
38713
|
+
}
|
|
38714
|
+
const sources = projectedSources?.filter((source) => check(ctx, source.path, "read").allow && check(ctx, source.path, "call").allow);
|
|
38715
|
+
const visiblePartial = sources?.some((source) => source.status !== "ok") === true;
|
|
38716
|
+
return {
|
|
38717
|
+
items,
|
|
38718
|
+
...page.cursor === void 0 ? {} : { cursor: page.cursor },
|
|
38719
|
+
...visiblePartial ? { partial: true } : {},
|
|
38720
|
+
...sources === void 0 ? {} : { sources }
|
|
38721
|
+
};
|
|
38722
|
+
}
|
|
38723
|
+
async function remoteResponse(deps, input, node, requestPath, method, headers, deadline, body) {
|
|
38724
|
+
if (node.config?.kind !== "remote") throw invalidContinuation();
|
|
38725
|
+
return await passthroughRemote({
|
|
38726
|
+
actor: {
|
|
38727
|
+
keyId: input.ctx.keyId,
|
|
38728
|
+
owner: input.ctx.owner,
|
|
38729
|
+
traceId: input.ctx.traceId
|
|
38730
|
+
},
|
|
38731
|
+
...body === void 0 ? {} : { body },
|
|
38732
|
+
config: node.config,
|
|
38733
|
+
headers,
|
|
38734
|
+
maxResponseBodyBytes: federatedSearchSettings(deps.remote).maxResponseBodyBytes,
|
|
38735
|
+
method,
|
|
38736
|
+
nodePath: node.path,
|
|
38737
|
+
requestPath,
|
|
38738
|
+
requestUrl: input.requestUrl,
|
|
38739
|
+
secrets: deps.secrets,
|
|
38740
|
+
settings: await resolveRemoteSettings(input.state, deps.remote),
|
|
38741
|
+
signal: deadlineSignal(deadline)
|
|
38742
|
+
});
|
|
38743
|
+
}
|
|
38744
|
+
async function assertFederatedCapability(deps, input, node, headers, deadline) {
|
|
38745
|
+
if (node.config?.kind !== "remote" || node.config.skRef === void 0) {
|
|
38746
|
+
throw new TBError("unavailable", "remote search source is unsupported", { retryable: false });
|
|
38747
|
+
}
|
|
38748
|
+
const response = await remoteResponse(
|
|
38749
|
+
deps,
|
|
38750
|
+
input,
|
|
38751
|
+
node,
|
|
38752
|
+
`${node.path}/~describe`,
|
|
38753
|
+
"GET",
|
|
38754
|
+
headers,
|
|
38755
|
+
deadline
|
|
38756
|
+
);
|
|
38757
|
+
if (!response.ok) {
|
|
38758
|
+
throw new TBError("unavailable", "remote search capability is unavailable", {
|
|
38759
|
+
retryable: response.status >= 500
|
|
38760
|
+
});
|
|
38761
|
+
}
|
|
38762
|
+
const value = await response.json().catch(() => null);
|
|
38763
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.capabilities) || !value.capabilities.includes("search:federated")) {
|
|
38764
|
+
throw new TBError("unavailable", "remote search source is unsupported", { retryable: false });
|
|
38765
|
+
}
|
|
38766
|
+
}
|
|
38767
|
+
function childSearchRequest(input, source, cursor) {
|
|
38768
|
+
const opts = input.request.opts ?? {};
|
|
38769
|
+
const childPrefix = childPrefixFor(source.path, opts.pathPrefix).pathPrefix;
|
|
38770
|
+
return {
|
|
38771
|
+
query: input.request.query,
|
|
38772
|
+
opts: {
|
|
38773
|
+
...opts.effects === void 0 ? {} : { effects: opts.effects },
|
|
38774
|
+
...cursor === void 0 ? {} : { cursor },
|
|
38775
|
+
detail: "compact",
|
|
38776
|
+
federation: "recursive",
|
|
38777
|
+
limit: 1,
|
|
38778
|
+
matching: opts.matching ?? "best",
|
|
38779
|
+
...opts.minCoverage === void 0 ? {} : { minCoverage: opts.minCoverage },
|
|
38780
|
+
mode: "keyword",
|
|
38781
|
+
...childPrefix === void 0 ? {} : { pathPrefix: childPrefix }
|
|
38782
|
+
}
|
|
38783
|
+
};
|
|
38784
|
+
}
|
|
38785
|
+
async function throwChildSearchError(response) {
|
|
38786
|
+
const error = await response.json().catch(() => null);
|
|
38787
|
+
const message = typeof error?.message === "string" ? error.message : "";
|
|
38788
|
+
if (message.includes("\u73AF")) {
|
|
38789
|
+
throw new TBError("unavailable", "\u68C0\u6D4B\u5230\u900F\u4F20\u73AF", { retryable: false });
|
|
38790
|
+
}
|
|
38791
|
+
if (message.includes("\u8DF3\u6570")) {
|
|
38792
|
+
throw new TBError("unavailable", "\u900F\u4F20\u8DF3\u6570\u8D85\u9650", { retryable: false });
|
|
38793
|
+
}
|
|
38794
|
+
if (response.status >= 400 && response.status < 500) throw invalidContinuation();
|
|
38795
|
+
throw new TBError("unavailable", "remote search unavailable", {
|
|
38796
|
+
retryable: response.status >= 500
|
|
38797
|
+
});
|
|
38798
|
+
}
|
|
38799
|
+
async function fetchHead(deps, input, source, remoteNodes, deadline, expiresAt, totalTermCount, skipBudget = 20) {
|
|
38800
|
+
if (source.done || source.pending !== void 0) return { statuses: [] };
|
|
38801
|
+
const opts = input.request.opts ?? {};
|
|
38802
|
+
if (source.kind === "local") {
|
|
38803
|
+
const page2 = await executeLocalSearch(
|
|
38804
|
+
{
|
|
38805
|
+
ctx: input.ctx,
|
|
38806
|
+
search: input.search,
|
|
38807
|
+
state: input.state
|
|
38808
|
+
},
|
|
38809
|
+
{
|
|
38810
|
+
query: input.request.query,
|
|
38811
|
+
opts: {
|
|
38812
|
+
...opts.effects === void 0 ? {} : { effects: opts.effects },
|
|
38813
|
+
...source.cursor === void 0 ? {} : { cursor: source.cursor },
|
|
38814
|
+
detail: "compact",
|
|
38815
|
+
federation: "local",
|
|
38816
|
+
limit: 1,
|
|
38817
|
+
matching: opts.matching ?? "best",
|
|
38818
|
+
...opts.minCoverage === void 0 ? {} : { minCoverage: opts.minCoverage },
|
|
38819
|
+
mode: "keyword",
|
|
38820
|
+
...opts.pathPrefix === void 0 ? {} : { pathPrefix: opts.pathPrefix }
|
|
38821
|
+
}
|
|
38822
|
+
}
|
|
38823
|
+
);
|
|
38824
|
+
const item2 = page2.items[0];
|
|
38825
|
+
if (item2 === void 0) {
|
|
38826
|
+
source.done = page2.cursor === void 0;
|
|
38827
|
+
source.cursor = page2.cursor;
|
|
38828
|
+
return { statuses: [] };
|
|
38829
|
+
}
|
|
38830
|
+
source.pending = compactItem({ ...item2, source: { path: "" } });
|
|
38831
|
+
source.nextCursor = page2.cursor;
|
|
38832
|
+
return { statuses: [] };
|
|
38833
|
+
}
|
|
38834
|
+
const node = remoteNodes.get(source.path);
|
|
38835
|
+
if (node === void 0 || node.config?.kind !== "remote" || !check(input.ctx, node.path, "read").allow || !check(input.ctx, node.path, "call").allow) {
|
|
38836
|
+
throw invalidContinuation();
|
|
38837
|
+
}
|
|
38838
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
38839
|
+
const remainingMs = deadline - Date.now() - settings.perHopReturnReserveMs;
|
|
38840
|
+
if (remainingMs < settings.minChildWorkMs) {
|
|
38841
|
+
throw new TBError("unavailable", "federated search child has no remaining work budget", {
|
|
38842
|
+
retryable: false
|
|
38843
|
+
});
|
|
38844
|
+
}
|
|
38845
|
+
const headers = federatedHeaders(
|
|
38846
|
+
input.headers,
|
|
38847
|
+
remainingMs,
|
|
38848
|
+
source.budget,
|
|
38849
|
+
expiresAt - Date.now()
|
|
38850
|
+
);
|
|
38851
|
+
const childRequest = childSearchRequest(input, source, source.cursor);
|
|
38852
|
+
const response = await remoteResponse(
|
|
38853
|
+
deps,
|
|
38854
|
+
input,
|
|
38855
|
+
node,
|
|
38856
|
+
`${node.path}/~search`,
|
|
38857
|
+
"POST",
|
|
38858
|
+
headers,
|
|
38859
|
+
deadline,
|
|
38860
|
+
JSON.stringify(childRequest)
|
|
38861
|
+
);
|
|
38862
|
+
if (!response.ok) await throwChildSearchError(response);
|
|
38863
|
+
const parsed = toolSearchPageSchema.safeParse(await response.json().catch(() => null));
|
|
38864
|
+
if (!parsed.success) {
|
|
38865
|
+
throw new TBError("unavailable", "remote search returned invalid JSON", { retryable: false });
|
|
38866
|
+
}
|
|
38867
|
+
if (parsed.data.items.length > 1) {
|
|
38868
|
+
throw new TBError("unavailable", "remote search exceeded the requested source batch size", {
|
|
38869
|
+
retryable: false
|
|
38870
|
+
});
|
|
38871
|
+
}
|
|
38872
|
+
if (!isFederatedSearchSessionHandle(parsed.data.snapshot)) {
|
|
38873
|
+
throw new TBError("unavailable", "remote search omitted its validation snapshot", {
|
|
38874
|
+
retryable: false
|
|
38875
|
+
});
|
|
38876
|
+
}
|
|
38877
|
+
source.validationCursor = parsed.data.snapshot;
|
|
38878
|
+
const page = projectChildPage(
|
|
38879
|
+
source.path,
|
|
38880
|
+
parsed.data,
|
|
38881
|
+
totalTermCount,
|
|
38882
|
+
input.ctx,
|
|
38883
|
+
input.request
|
|
38884
|
+
);
|
|
38885
|
+
const item = page.items[0];
|
|
38886
|
+
if (item === void 0) {
|
|
38887
|
+
source.done = page.cursor === void 0;
|
|
38888
|
+
source.cursor = page.cursor;
|
|
38889
|
+
if (!source.done) {
|
|
38890
|
+
if (skipBudget <= 1) {
|
|
38891
|
+
throw new TBError("unavailable", "federated search source work budget exhausted", {
|
|
38892
|
+
retryable: false
|
|
38893
|
+
});
|
|
38894
|
+
}
|
|
38895
|
+
return await fetchHead(
|
|
38896
|
+
deps,
|
|
38897
|
+
input,
|
|
38898
|
+
source,
|
|
38899
|
+
remoteNodes,
|
|
38900
|
+
deadline,
|
|
38901
|
+
expiresAt,
|
|
38902
|
+
totalTermCount,
|
|
38903
|
+
skipBudget - 1
|
|
38904
|
+
);
|
|
38905
|
+
}
|
|
38906
|
+
} else {
|
|
38907
|
+
source.pending = item;
|
|
38908
|
+
source.nextCursor = page.cursor;
|
|
38909
|
+
}
|
|
38910
|
+
return { statuses: page.sources?.filter((status) => status.status !== "ok") ?? [] };
|
|
38911
|
+
}
|
|
38912
|
+
async function validateSourceSnapshots(deps, input, sources, remoteNodes, deadline, expiresAt, maxConcurrency) {
|
|
38913
|
+
const remotes = sources.filter((source) => source.kind === "remote" && source.excluded !== true);
|
|
38914
|
+
await mapLimit(remotes, maxConcurrency, async (source) => {
|
|
38915
|
+
const node = remoteNodes.get(source.path);
|
|
38916
|
+
if (node === void 0 || source.validationCursor === void 0) throw invalidContinuation();
|
|
38917
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
38918
|
+
const remainingMs = deadline - Date.now() - settings.perHopReturnReserveMs;
|
|
38919
|
+
if (remainingMs < settings.minChildWorkMs) throw retryableContinuationError();
|
|
38920
|
+
const headers = federatedHeaders(
|
|
38921
|
+
input.headers,
|
|
38922
|
+
remainingMs,
|
|
38923
|
+
source.budget,
|
|
38924
|
+
expiresAt - Date.now()
|
|
38925
|
+
);
|
|
38926
|
+
headers.set(HEADER_VALIDATE_SNAPSHOT, "1");
|
|
38927
|
+
const response = await remoteResponse(
|
|
38928
|
+
deps,
|
|
38929
|
+
input,
|
|
38930
|
+
node,
|
|
38931
|
+
`${node.path}/~search`,
|
|
38932
|
+
"POST",
|
|
38933
|
+
headers,
|
|
38934
|
+
deadline,
|
|
38935
|
+
JSON.stringify(childSearchRequest(input, source, source.validationCursor))
|
|
38936
|
+
);
|
|
38937
|
+
if (!response.ok) await throwChildSearchError(response);
|
|
38938
|
+
const parsed = toolSearchPageSchema.safeParse(await response.json().catch(() => null));
|
|
38939
|
+
if (!parsed.success || parsed.data.items.length !== 0 || parsed.data.cursor !== void 0 || parsed.data.snapshot !== void 0 || parsed.data.partial !== void 0 || parsed.data.sources !== void 0) throw invalidContinuation();
|
|
38940
|
+
});
|
|
38941
|
+
}
|
|
38942
|
+
function consume(source) {
|
|
38943
|
+
const item = source.pending;
|
|
38944
|
+
if (item === void 0) throw new Error("federated source has no pending item");
|
|
38945
|
+
source.pending = void 0;
|
|
38946
|
+
source.cursor = source.nextCursor;
|
|
38947
|
+
source.nextCursor = void 0;
|
|
38948
|
+
source.done = source.cursor === void 0;
|
|
38949
|
+
return item;
|
|
38950
|
+
}
|
|
38951
|
+
async function hydrateLocal(input, item) {
|
|
38952
|
+
if (!check(input.ctx, item.path, "read").allow || !check(input.ctx, item.path, "call").allow) {
|
|
38953
|
+
throw invalidContinuation();
|
|
38954
|
+
}
|
|
38955
|
+
const registry = new NodeRegistryStore(input.state);
|
|
38956
|
+
const node = await registry.get(item.path).catch(() => null);
|
|
38957
|
+
if (node === null || node.kind !== "mcp" && node.kind !== "http" && node.kind !== "tool" || node.config?.kind !== node.kind) {
|
|
38958
|
+
throw invalidContinuation();
|
|
38959
|
+
}
|
|
38960
|
+
const raw = (await canonicalSearchTools(input.state, [node])).get(node.path);
|
|
38961
|
+
if (raw === void 0) throw invalidContinuation();
|
|
38962
|
+
const matches = virtualizeTools(node.virtualize, raw).exposed.filter((tool2) => tool2.name === item.tool.name);
|
|
38963
|
+
if (matches.length !== 1) throw invalidContinuation();
|
|
38964
|
+
const tool = matches[0];
|
|
38965
|
+
if (tool === void 0) throw invalidContinuation();
|
|
38966
|
+
return { ...item, tool };
|
|
38967
|
+
}
|
|
38968
|
+
async function hydrateRemote(deps, input, item, source, node, deadline) {
|
|
38969
|
+
if (!check(input.ctx, source.path, "read").allow || !check(input.ctx, source.path, "call").allow || !check(input.ctx, item.path, "read").allow || !check(input.ctx, item.path, "call").allow) {
|
|
38970
|
+
throw invalidContinuation();
|
|
38971
|
+
}
|
|
38972
|
+
const response = await remoteResponse(
|
|
38973
|
+
deps,
|
|
38974
|
+
input,
|
|
38975
|
+
node,
|
|
38976
|
+
`${item.path}/${item.tool.name}/~help`,
|
|
38977
|
+
"GET",
|
|
38978
|
+
(() => {
|
|
38979
|
+
const headers = new Headers({ accept: "application/json" });
|
|
38980
|
+
const via = input.headers.get(HEADER_VIA);
|
|
38981
|
+
if (via !== null) headers.set(HEADER_VIA, via);
|
|
38982
|
+
headers.set(
|
|
38983
|
+
HEADER_REMAINING_MS,
|
|
38984
|
+
String(Math.max(1, Math.floor(deadline - Date.now())))
|
|
38985
|
+
);
|
|
38986
|
+
return headers;
|
|
38987
|
+
})(),
|
|
38988
|
+
deadline
|
|
38989
|
+
);
|
|
38990
|
+
if (!response.ok) {
|
|
38991
|
+
if (response.status >= 400 && response.status < 500) throw invalidContinuation();
|
|
38992
|
+
throw retryableContinuationError();
|
|
38993
|
+
}
|
|
38994
|
+
const parsed = helpJsonSchema.safeParse(await response.json().catch(() => null));
|
|
38995
|
+
if (!parsed.success) throw invalidContinuation();
|
|
38996
|
+
const projected = new RemotePathProjector(source.path).projectHelp(parsed.data, `${item.path}/${item.tool.name}`);
|
|
38997
|
+
const commands = projected.cmds.filter((cmd) => cmd.name === item.tool.name && cmd.path === `/${item.path}/${item.tool.name}` && cmd.scope === "call");
|
|
38998
|
+
if (commands.length !== 1) throw invalidContinuation();
|
|
38999
|
+
const command = commands[0];
|
|
39000
|
+
if (command === void 0) throw invalidContinuation();
|
|
39001
|
+
const tool = {
|
|
39002
|
+
name: command.name,
|
|
39003
|
+
...command.h === void 0 ? {} : { description: command.h },
|
|
39004
|
+
...command.effect === void 0 ? {} : { effect: command.effect },
|
|
39005
|
+
...command.confirm === void 0 ? {} : { confirm: command.confirm },
|
|
39006
|
+
...command.inputSchema === void 0 ? {} : { inputSchema: command.inputSchema },
|
|
39007
|
+
...command.outputSchema === void 0 ? {} : { outputSchema: command.outputSchema }
|
|
39008
|
+
};
|
|
39009
|
+
return { ...item, tool };
|
|
39010
|
+
}
|
|
39011
|
+
async function computePage(deps, input, sources, remoteNodes, deadline, expiresAt, totalTermCount, initial, initialExcluded, maxConcurrency) {
|
|
39012
|
+
let excluded = [...initialExcluded];
|
|
39013
|
+
if (!initial) {
|
|
39014
|
+
await validateSourceSnapshots(
|
|
39015
|
+
deps,
|
|
39016
|
+
input,
|
|
39017
|
+
sources,
|
|
39018
|
+
remoteNodes,
|
|
39019
|
+
deadline,
|
|
39020
|
+
expiresAt,
|
|
39021
|
+
maxConcurrency
|
|
39022
|
+
);
|
|
39023
|
+
}
|
|
39024
|
+
const active = sources.filter((source) => !source.done);
|
|
39025
|
+
const capabilityFailures = await mapLimit(
|
|
39026
|
+
initial ? active.filter((source) => source.kind === "remote") : [],
|
|
39027
|
+
maxConcurrency,
|
|
39028
|
+
async (source) => {
|
|
39029
|
+
const node = remoteNodes.get(source.path);
|
|
39030
|
+
if (node === void 0) return { source, status: "invalid_response" };
|
|
39031
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
39032
|
+
const remainingMs = deadline - Date.now() - settings.perHopReturnReserveMs;
|
|
39033
|
+
if (remainingMs < settings.minChildWorkMs) {
|
|
39034
|
+
return { source, status: "budget_exhausted" };
|
|
39035
|
+
}
|
|
39036
|
+
const headers = federatedHeaders(
|
|
39037
|
+
input.headers,
|
|
39038
|
+
remainingMs,
|
|
39039
|
+
source.budget,
|
|
39040
|
+
expiresAt - Date.now()
|
|
39041
|
+
);
|
|
39042
|
+
try {
|
|
39043
|
+
await assertFederatedCapability(deps, input, node, headers, deadline);
|
|
39044
|
+
return null;
|
|
39045
|
+
} catch (error) {
|
|
39046
|
+
const status = error instanceof TBError && error.message.includes("unsupported") ? "unsupported" : controlledStatus(error);
|
|
39047
|
+
return { error, source, status };
|
|
39048
|
+
}
|
|
39049
|
+
}
|
|
39050
|
+
);
|
|
39051
|
+
for (const failure of capabilityFailures) {
|
|
39052
|
+
if (failure === null) continue;
|
|
39053
|
+
if (!initial) {
|
|
39054
|
+
if (failure.error instanceof TBError && failure.error.code === "invalid_argument") {
|
|
39055
|
+
throw failure.error;
|
|
39056
|
+
}
|
|
39057
|
+
throw failure.status === "unsupported" ? invalidContinuation() : retryableContinuationError();
|
|
39058
|
+
}
|
|
39059
|
+
failure.source.done = true;
|
|
39060
|
+
failure.source.excluded = true;
|
|
39061
|
+
excluded = mergeStatuses(excluded, [{ path: failure.source.path, status: failure.status }]);
|
|
39062
|
+
}
|
|
39063
|
+
const storedPending = sources.filter((source) => source.pending !== void 0);
|
|
39064
|
+
const refreshedStatuses = await mapLimit(storedPending, maxConcurrency, async (source) => {
|
|
39065
|
+
try {
|
|
39066
|
+
const pending = source.pending;
|
|
39067
|
+
if (pending === void 0) throw invalidContinuation();
|
|
39068
|
+
const hydrated = source.kind === "local" ? await hydrateLocal(input, pending) : await (async () => {
|
|
39069
|
+
const node = remoteNodes.get(source.path);
|
|
39070
|
+
if (node === void 0) throw invalidContinuation();
|
|
39071
|
+
return await hydrateRemote(deps, input, pending, source, node, deadline);
|
|
39072
|
+
})();
|
|
39073
|
+
if (!sameJson(compactItem(hydrated), pending)) throw invalidContinuation();
|
|
39074
|
+
return [];
|
|
39075
|
+
} catch (error) {
|
|
39076
|
+
throw continuationSourceError(error);
|
|
39077
|
+
}
|
|
39078
|
+
});
|
|
39079
|
+
excluded = mergeStatuses(excluded, ...refreshedStatuses);
|
|
39080
|
+
let successfulSources = storedPending.length;
|
|
39081
|
+
const headResults = await mapLimit(
|
|
39082
|
+
sources.filter((source) => !source.done && source.pending === void 0),
|
|
39083
|
+
maxConcurrency,
|
|
39084
|
+
async (source) => {
|
|
39085
|
+
try {
|
|
39086
|
+
const result = await fetchHead(
|
|
39087
|
+
deps,
|
|
39088
|
+
input,
|
|
39089
|
+
source,
|
|
39090
|
+
remoteNodes,
|
|
39091
|
+
deadline,
|
|
39092
|
+
expiresAt,
|
|
39093
|
+
totalTermCount
|
|
39094
|
+
);
|
|
39095
|
+
return { ...result, success: true };
|
|
39096
|
+
} catch (error) {
|
|
39097
|
+
if (!initial) throw continuationSourceError(error);
|
|
39098
|
+
source.done = true;
|
|
39099
|
+
source.excluded = true;
|
|
39100
|
+
return {
|
|
39101
|
+
statuses: [{ path: source.path, status: controlledStatus(error) }],
|
|
39102
|
+
success: false
|
|
39103
|
+
};
|
|
39104
|
+
}
|
|
39105
|
+
}
|
|
39106
|
+
);
|
|
39107
|
+
successfulSources += headResults.filter((result) => result.success).length;
|
|
39108
|
+
excluded = mergeStatuses(excluded, ...headResults.map((result) => result.statuses));
|
|
39109
|
+
const items = [];
|
|
39110
|
+
let selectedCoverage;
|
|
39111
|
+
const limit = input.request.opts?.limit ?? 10;
|
|
39112
|
+
while (items.length < limit) {
|
|
39113
|
+
const heads = sources.filter((source2) => source2.pending !== void 0);
|
|
39114
|
+
if (heads.length === 0) break;
|
|
39115
|
+
const highest = Math.max(...heads.map((source2) => source2.pending?.relevance.coverage ?? -1));
|
|
39116
|
+
selectedCoverage ??= highest;
|
|
39117
|
+
if ((input.request.opts?.matching ?? "best") === "best" && highest !== selectedCoverage) break;
|
|
39118
|
+
const bucket = heads.filter((source2) => source2.pending?.relevance.coverage === highest).sort((left, right) => (left.balanceCoverage === highest ? left.balanceCount : 0) - (right.balanceCoverage === highest ? right.balanceCount : 0) || left.path.localeCompare(right.path));
|
|
39119
|
+
const source = bucket[0];
|
|
39120
|
+
if (source === void 0) break;
|
|
39121
|
+
items.push(consume(source));
|
|
39122
|
+
if (source.balanceCoverage !== highest) {
|
|
39123
|
+
source.balanceCoverage = highest;
|
|
39124
|
+
source.balanceCount = 0;
|
|
39125
|
+
}
|
|
39126
|
+
source.balanceCount++;
|
|
39127
|
+
try {
|
|
39128
|
+
const result = await fetchHead(
|
|
39129
|
+
deps,
|
|
39130
|
+
input,
|
|
39131
|
+
source,
|
|
39132
|
+
remoteNodes,
|
|
39133
|
+
deadline,
|
|
39134
|
+
expiresAt,
|
|
39135
|
+
totalTermCount
|
|
39136
|
+
);
|
|
39137
|
+
excluded = mergeStatuses(excluded, result.statuses);
|
|
39138
|
+
} catch (error) {
|
|
39139
|
+
if (!initial) throw continuationSourceError(error);
|
|
39140
|
+
source.done = true;
|
|
39141
|
+
source.excluded = true;
|
|
39142
|
+
excluded = mergeStatuses(excluded, [{ path: source.path, status: controlledStatus(error) }]);
|
|
39143
|
+
}
|
|
39144
|
+
}
|
|
39145
|
+
const partial = excluded.length > 0;
|
|
39146
|
+
const page = {
|
|
39147
|
+
items,
|
|
39148
|
+
...partial ? { partial: true, sources: excluded } : {}
|
|
39149
|
+
};
|
|
39150
|
+
return { excluded, page, sources, successfulSources };
|
|
39151
|
+
}
|
|
39152
|
+
function visibleSourceStatuses(ctx, page) {
|
|
39153
|
+
const rawSources = page.sources;
|
|
39154
|
+
const rest = { ...page };
|
|
39155
|
+
delete rest.partial;
|
|
39156
|
+
delete rest.sources;
|
|
39157
|
+
const sources = rawSources?.filter((source) => check(ctx, source.path, "read").allow && check(ctx, source.path, "call").allow);
|
|
39158
|
+
const partial = sources?.some((source) => source.status !== "ok") === true;
|
|
39159
|
+
return {
|
|
39160
|
+
...rest,
|
|
39161
|
+
...partial ? { partial: true } : {},
|
|
39162
|
+
...sources === void 0 || sources.length === 0 ? {} : { sources }
|
|
39163
|
+
};
|
|
39164
|
+
}
|
|
39165
|
+
async function hydratePage(deps, input, page, sources, remoteNodes, deadline, forceRevalidate = false) {
|
|
39166
|
+
const visiblePage = visibleSourceStatuses(input.ctx, page);
|
|
39167
|
+
const detail = input.request.opts?.detail ?? "compact";
|
|
39168
|
+
if (detail !== "full" && !forceRevalidate) return visiblePage;
|
|
39169
|
+
const constraints = requestConstraints(input.request);
|
|
39170
|
+
const output = [];
|
|
39171
|
+
const encoder = new TextEncoder();
|
|
39172
|
+
let outputBytes = encoder.encode(JSON.stringify({ ...visiblePage, items: [] })).length;
|
|
39173
|
+
const concurrency = federatedSearchSettings(deps.remote).maxConcurrency;
|
|
39174
|
+
for (let offset = 0; offset < visiblePage.items.length; offset += concurrency) {
|
|
39175
|
+
const batch = visiblePage.items.slice(offset, offset + concurrency);
|
|
39176
|
+
const hydrated = await Promise.all(batch.map(async (item) => {
|
|
39177
|
+
const direct = sources.find((source) => source.kind === "remote" && isPrefix(source.path, item.path));
|
|
39178
|
+
const full = direct === void 0 ? await hydrateLocal(input, item) : await (async () => {
|
|
39179
|
+
const node = remoteNodes.get(direct.path);
|
|
39180
|
+
if (node === void 0) throw invalidContinuation();
|
|
39181
|
+
return await hydrateRemote(deps, input, item, direct, node, deadline);
|
|
39182
|
+
})();
|
|
39183
|
+
if (!sameJson(compactItem(full), compactItem(item))) throw invalidContinuation();
|
|
39184
|
+
assertHardResultConstraints(full, constraints);
|
|
39185
|
+
return full;
|
|
39186
|
+
}));
|
|
39187
|
+
if (detail !== "full") continue;
|
|
39188
|
+
for (const item of hydrated) {
|
|
39189
|
+
outputBytes += encoder.encode(JSON.stringify(item)).length;
|
|
39190
|
+
if (outputBytes > TOOL_SEARCH_PAGE_BYTES) {
|
|
39191
|
+
throw new TBError("rate_limited", "federated search page exceeds response byte budget", {
|
|
39192
|
+
retryable: false
|
|
39193
|
+
});
|
|
39194
|
+
}
|
|
39195
|
+
output.push(item);
|
|
39196
|
+
}
|
|
39197
|
+
}
|
|
39198
|
+
return detail === "full" ? { ...visiblePage, items: output } : visiblePage;
|
|
39199
|
+
}
|
|
39200
|
+
function pageWithCursor(page, cursor) {
|
|
39201
|
+
return cursor === void 0 ? page : { ...page, cursor };
|
|
39202
|
+
}
|
|
39203
|
+
function assertPageBytes(page) {
|
|
39204
|
+
if (new TextEncoder().encode(JSON.stringify(page)).length > TOOL_SEARCH_PAGE_BYTES) {
|
|
39205
|
+
throw new TBError("rate_limited", "federated search page exceeds response byte budget", {
|
|
39206
|
+
retryable: false
|
|
39207
|
+
});
|
|
39208
|
+
}
|
|
39209
|
+
}
|
|
39210
|
+
async function executeFederatedSearch(deps, input) {
|
|
39211
|
+
if (deps.remote.instanceId === void 0 || input.state.compareAndSwap === void 0) {
|
|
39212
|
+
throw new TBError(
|
|
39213
|
+
"invalid_argument",
|
|
39214
|
+
"recursive search requires explicit TB_INSTANCE_ID and atomic StateStore CAS"
|
|
39215
|
+
);
|
|
39216
|
+
}
|
|
39217
|
+
if ((input.request.opts?.mode ?? "keyword") !== "keyword") {
|
|
39218
|
+
throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
|
|
39219
|
+
}
|
|
39220
|
+
const wireOpts = input.request.opts ?? {};
|
|
39221
|
+
const constraints = normalizeToolSearchOptions({
|
|
39222
|
+
...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
|
|
39223
|
+
matching: wireOpts.matching ?? "best",
|
|
39224
|
+
...wireOpts.minCoverage === void 0 ? {} : { minCoverage: wireOpts.minCoverage },
|
|
39225
|
+
...wireOpts.pathPrefix === void 0 ? {} : { pathPrefix: wireOpts.pathPrefix }
|
|
39226
|
+
});
|
|
39227
|
+
input = {
|
|
39228
|
+
...input,
|
|
39229
|
+
request: {
|
|
39230
|
+
query: input.request.query,
|
|
39231
|
+
opts: {
|
|
39232
|
+
...wireOpts,
|
|
39233
|
+
...constraints.effects === void 0 ? {} : { effects: constraints.effects },
|
|
39234
|
+
matching: constraints.matching,
|
|
39235
|
+
...constraints.minCoverage === void 0 ? {} : { minCoverage: constraints.minCoverage },
|
|
39236
|
+
...constraints.pathPrefix === void 0 ? {} : { pathPrefix: constraints.pathPrefix }
|
|
39237
|
+
}
|
|
39238
|
+
}
|
|
39239
|
+
};
|
|
39240
|
+
const viaError = checkVia(
|
|
39241
|
+
parseVia(input.headers.get(HEADER_VIA) ?? void 0),
|
|
39242
|
+
deps.remote.instanceId,
|
|
39243
|
+
deps.remote.maxHops
|
|
39244
|
+
);
|
|
39245
|
+
if (viaError !== null) throw viaError;
|
|
39246
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
39247
|
+
await input.searchSync?.ensureReady();
|
|
39248
|
+
if (input.search.revision === void 0) {
|
|
39249
|
+
throw new TBError("invalid_argument", "recursive search requires a revisioned search index");
|
|
39250
|
+
}
|
|
39251
|
+
const localRevision = await input.search.revision();
|
|
39252
|
+
const deadlineMs = clampHeader(input.headers.get(HEADER_REMAINING_MS), settings.totalDeadlineMs);
|
|
39253
|
+
const sessionTtlMs = clampHeader(input.headers.get(HEADER_SESSION_TTL_MS), settings.sessionTtlMs);
|
|
39254
|
+
const sourceBudget = clampHeader(input.headers.get(HEADER_SOURCE_BUDGET), settings.maxSources);
|
|
39255
|
+
const deadline = Date.now() + deadlineMs;
|
|
39256
|
+
const expiresAtMs = Date.now() + sessionTtlMs;
|
|
39257
|
+
const limit = input.request.opts?.limit ?? 10;
|
|
39258
|
+
const requestDigest = await sha256Hex(canonicalJson({
|
|
39259
|
+
authorization: input.ctx.scopes,
|
|
39260
|
+
request: requestForDigest(input.request, limit)
|
|
39261
|
+
}));
|
|
39262
|
+
const sessionStore = new FederatedSearchSessionStore(input.state);
|
|
39263
|
+
const plan = prepareToolSearchQuery(input.request.query);
|
|
39264
|
+
const registry = new NodeRegistryStore(input.state);
|
|
39265
|
+
const snapshot = await registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
|
|
39266
|
+
const allDirect = directRemoteNodes(snapshot.items);
|
|
39267
|
+
const eligibleRemotes = allDirect.filter((node) => {
|
|
39268
|
+
const relation = childPrefixFor(node.path, input.request.opts?.pathPrefix);
|
|
39269
|
+
return relation.eligible && check(input.ctx, node.path, "read").allow && check(input.ctx, node.path, "call").allow;
|
|
39270
|
+
});
|
|
39271
|
+
const includeLocal = localEligible(allDirect, input.request.opts?.pathPrefix);
|
|
39272
|
+
const configuredRemotes = eligibleRemotes.filter((node) => node.config?.kind === "remote" && node.config.skRef !== void 0);
|
|
39273
|
+
const secretAvailability = await mapLimit(
|
|
39274
|
+
configuredRemotes,
|
|
39275
|
+
settings.maxConcurrency,
|
|
39276
|
+
async (node) => {
|
|
39277
|
+
if (node.config?.kind !== "remote" || node.config.skRef === void 0) return false;
|
|
39278
|
+
try {
|
|
39279
|
+
return await deps.secrets.resolve(node.config.skRef) !== void 0;
|
|
39280
|
+
} catch {
|
|
39281
|
+
return false;
|
|
39282
|
+
}
|
|
39283
|
+
}
|
|
39284
|
+
);
|
|
39285
|
+
const participatingRemotes = configuredRemotes.filter((_node, index) => secretAvailability[index] === true);
|
|
39286
|
+
const unavailableRemotes = configuredRemotes.filter((_node, index) => secretAvailability[index] !== true);
|
|
39287
|
+
const transportSettings = await resolveRemoteSettings(input.state, deps.remote);
|
|
39288
|
+
const topologyNodes = snapshot.items.filter((node) => check(input.ctx, node.path, "read").allow && check(input.ctx, node.path, "call").allow);
|
|
39289
|
+
const digest = await topologyDigest(topologyNodes, includeLocal, localRevision, {
|
|
39290
|
+
allowInsecure: transportSettings.allowInsecure,
|
|
39291
|
+
allowlist: [...transportSettings.allowlist].sort(),
|
|
39292
|
+
availableRemotes: participatingRemotes.map((node) => node.path),
|
|
39293
|
+
instanceId: transportSettings.instanceId ?? null,
|
|
39294
|
+
maxHops: transportSettings.maxHops
|
|
39295
|
+
});
|
|
39296
|
+
const remoteNodes = new Map(participatingRemotes.map((node) => [node.path, node]));
|
|
39297
|
+
const cursor = input.request.opts?.cursor;
|
|
39298
|
+
if (cursor !== void 0) {
|
|
39299
|
+
const current = await sessionStore.read(cursor, { actorKeyId: input.ctx.keyId, requestDigest });
|
|
39300
|
+
if (current.topologyDigest !== digest || current.rankingVersion !== TOOL_SEARCH_RANKING_VERSION) {
|
|
39301
|
+
throw invalidContinuation();
|
|
39302
|
+
}
|
|
39303
|
+
if (input.headers.get(HEADER_VALIDATE_SNAPSHOT) === "1") {
|
|
39304
|
+
await validateSourceSnapshots(
|
|
39305
|
+
deps,
|
|
39306
|
+
input,
|
|
39307
|
+
current.sourceContinuations,
|
|
39308
|
+
remoteNodes,
|
|
39309
|
+
deadline,
|
|
39310
|
+
Date.parse(current.expiresAt),
|
|
39311
|
+
settings.maxConcurrency
|
|
39312
|
+
);
|
|
39313
|
+
return { items: [] };
|
|
39314
|
+
}
|
|
39315
|
+
if (current.nextHandle === null) throw invalidContinuation();
|
|
39316
|
+
let next;
|
|
39317
|
+
try {
|
|
39318
|
+
next = await sessionStore.read(current.nextHandle, {
|
|
39319
|
+
actorKeyId: input.ctx.keyId,
|
|
39320
|
+
requestDigest
|
|
39321
|
+
});
|
|
39322
|
+
} catch (error) {
|
|
39323
|
+
if (!(error instanceof TBError) || error.code !== "invalid_argument") throw error;
|
|
39324
|
+
}
|
|
39325
|
+
if (next !== void 0) assertNextRecord(current, next, digest);
|
|
39326
|
+
const cachedNext = next !== void 0;
|
|
39327
|
+
if (next === void 0) {
|
|
39328
|
+
const state2 = await computePage(
|
|
39329
|
+
deps,
|
|
39330
|
+
input,
|
|
39331
|
+
structuredClone(current.sourceContinuations),
|
|
39332
|
+
remoteNodes,
|
|
39333
|
+
deadline,
|
|
39334
|
+
Date.parse(current.expiresAt),
|
|
39335
|
+
plan.totalTermCount,
|
|
39336
|
+
false,
|
|
39337
|
+
current.excludedStatuses,
|
|
39338
|
+
settings.maxConcurrency
|
|
39339
|
+
);
|
|
39340
|
+
const hasMore2 = responseHasMore(state2.sources);
|
|
39341
|
+
const nextNextHandle = hasMore2 ? sessionStore.issueHandle() : null;
|
|
39342
|
+
next = await sessionStore.create(current.nextHandle, {
|
|
39343
|
+
actorKeyId: input.ctx.keyId,
|
|
39344
|
+
excludedStatuses: state2.excluded,
|
|
39345
|
+
expiresAt: current.expiresAt,
|
|
39346
|
+
federationPolicy: current.federationPolicy,
|
|
39347
|
+
generation: current.generation + 1,
|
|
39348
|
+
nextHandle: nextNextHandle,
|
|
39349
|
+
page: state2.page,
|
|
39350
|
+
rankingVersion: TOOL_SEARCH_RANKING_VERSION,
|
|
39351
|
+
requestDigest,
|
|
39352
|
+
sessionId: current.sessionId,
|
|
39353
|
+
sourceContinuations: serializableSources(state2.sources),
|
|
39354
|
+
topologyDigest: digest
|
|
39355
|
+
});
|
|
39356
|
+
assertNextRecord(current, next, digest);
|
|
39357
|
+
assertCachedPage(state2, next);
|
|
39358
|
+
}
|
|
39359
|
+
if (cachedNext) {
|
|
39360
|
+
await validateSourceSnapshots(
|
|
39361
|
+
deps,
|
|
39362
|
+
input,
|
|
39363
|
+
next.sourceContinuations,
|
|
39364
|
+
remoteNodes,
|
|
39365
|
+
deadline,
|
|
39366
|
+
Date.parse(next.expiresAt),
|
|
39367
|
+
settings.maxConcurrency
|
|
39368
|
+
);
|
|
39369
|
+
}
|
|
39370
|
+
const wantsSnapshot2 = input.headers.get(HEADER_WANT_SNAPSHOT) === "1";
|
|
39371
|
+
const responsePage2 = {
|
|
39372
|
+
...pageWithCursor(
|
|
39373
|
+
next.page,
|
|
39374
|
+
next.nextHandle === null ? void 0 : current.nextHandle
|
|
39375
|
+
),
|
|
39376
|
+
...wantsSnapshot2 ? { snapshot: current.nextHandle } : {}
|
|
39377
|
+
};
|
|
39378
|
+
const response2 = await hydratePage(
|
|
39379
|
+
deps,
|
|
39380
|
+
input,
|
|
39381
|
+
responsePage2,
|
|
39382
|
+
next.sourceContinuations,
|
|
39383
|
+
remoteNodes,
|
|
39384
|
+
deadline,
|
|
39385
|
+
cachedNext
|
|
39386
|
+
);
|
|
39387
|
+
assertPageBytes(response2);
|
|
39388
|
+
return response2;
|
|
39389
|
+
}
|
|
39390
|
+
const initialExcluded = eligibleRemotes.filter((node) => node.config?.kind === "remote" && node.config.skRef === void 0).map((node) => ({ path: node.path, status: "unsupported" }));
|
|
39391
|
+
initialExcluded.push(...unavailableRemotes.map((node) => ({
|
|
39392
|
+
path: node.path,
|
|
39393
|
+
status: "unavailable"
|
|
39394
|
+
})));
|
|
39395
|
+
const availableRemoteSlots = Math.max(0, sourceBudget - (includeLocal ? 1 : 0));
|
|
39396
|
+
const selectedRemotes = participatingRemotes.slice(0, availableRemoteSlots);
|
|
39397
|
+
for (const node of participatingRemotes.slice(availableRemoteSlots)) {
|
|
39398
|
+
initialExcluded.push({ path: node.path, status: "budget_exhausted" });
|
|
39399
|
+
}
|
|
39400
|
+
if (snapshot.truncated) initialExcluded.push({ path: "", status: "budget_exhausted" });
|
|
39401
|
+
const allocations = selectedRemotes.map((_node, index) => {
|
|
39402
|
+
const base = selectedRemotes.length === 0 ? 0 : Math.floor(availableRemoteSlots / selectedRemotes.length);
|
|
39403
|
+
const extra = index < availableRemoteSlots % Math.max(1, selectedRemotes.length) ? 1 : 0;
|
|
39404
|
+
return Math.max(1, base + extra);
|
|
39405
|
+
});
|
|
39406
|
+
const sources = [
|
|
39407
|
+
...includeLocal ? [{ balanceCount: 0, budget: 1, done: false, kind: "local", path: "" }] : [],
|
|
39408
|
+
...selectedRemotes.map((node, index) => ({
|
|
39409
|
+
balanceCount: 0,
|
|
39410
|
+
budget: allocations[index] ?? 1,
|
|
39411
|
+
done: false,
|
|
39412
|
+
kind: "remote",
|
|
39413
|
+
path: node.path
|
|
39414
|
+
}))
|
|
39415
|
+
];
|
|
39416
|
+
const state = await computePage(
|
|
39417
|
+
deps,
|
|
39418
|
+
input,
|
|
39419
|
+
sources,
|
|
39420
|
+
remoteNodes,
|
|
39421
|
+
deadline,
|
|
39422
|
+
expiresAtMs,
|
|
39423
|
+
plan.totalTermCount,
|
|
39424
|
+
true,
|
|
39425
|
+
initialExcluded,
|
|
39426
|
+
settings.maxConcurrency
|
|
39427
|
+
);
|
|
39428
|
+
const candidateSourceCount = (includeLocal ? 1 : 0) + eligibleRemotes.length;
|
|
39429
|
+
if (candidateSourceCount > 0 && state.successfulSources === 0 && state.page.items.length === 0) {
|
|
39430
|
+
throw new TBError("unavailable", "all federated search sources failed", { retryable: true });
|
|
39431
|
+
}
|
|
39432
|
+
const hasMore = responseHasMore(state.sources);
|
|
39433
|
+
const wantsSnapshot = input.headers.get(HEADER_WANT_SNAPSHOT) === "1";
|
|
39434
|
+
const needsSession = hasMore || wantsSnapshot;
|
|
39435
|
+
const sessionHandle = needsSession ? sessionStore.issueHandle() : void 0;
|
|
39436
|
+
const responseCursor = hasMore ? sessionHandle : void 0;
|
|
39437
|
+
const nextHandle = hasMore ? sessionStore.issueHandle() : null;
|
|
39438
|
+
const responsePage = {
|
|
39439
|
+
...pageWithCursor(state.page, responseCursor),
|
|
39440
|
+
...wantsSnapshot && sessionHandle !== void 0 ? { snapshot: sessionHandle } : {}
|
|
39441
|
+
};
|
|
39442
|
+
const response = await hydratePage(
|
|
39443
|
+
deps,
|
|
39444
|
+
input,
|
|
39445
|
+
responsePage,
|
|
39446
|
+
state.sources,
|
|
39447
|
+
remoteNodes,
|
|
39448
|
+
deadline
|
|
39449
|
+
);
|
|
39450
|
+
assertPageBytes(response);
|
|
39451
|
+
if (sessionHandle !== void 0) {
|
|
39452
|
+
await sessionStore.create(sessionHandle, {
|
|
39453
|
+
actorKeyId: input.ctx.keyId,
|
|
39454
|
+
excludedStatuses: state.excluded,
|
|
39455
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
39456
|
+
federationPolicy: { limit, matching: input.request.opts?.matching ?? "best" },
|
|
39457
|
+
generation: 0,
|
|
39458
|
+
nextHandle,
|
|
39459
|
+
page: state.page,
|
|
39460
|
+
rankingVersion: TOOL_SEARCH_RANKING_VERSION,
|
|
39461
|
+
requestDigest,
|
|
39462
|
+
sourceContinuations: serializableSources(state.sources),
|
|
39463
|
+
topologyDigest: digest
|
|
39464
|
+
});
|
|
39465
|
+
}
|
|
39466
|
+
return response;
|
|
39467
|
+
}
|
|
39468
|
+
|
|
37598
39469
|
// ../app/src/routes/search.ts
|
|
39470
|
+
function requestWithNormalizedLimit(request) {
|
|
39471
|
+
return {
|
|
39472
|
+
...request,
|
|
39473
|
+
opts: {
|
|
39474
|
+
...request.opts ?? {},
|
|
39475
|
+
limit: normalizeToolSearchLimit(request.opts?.limit)
|
|
39476
|
+
}
|
|
39477
|
+
};
|
|
39478
|
+
}
|
|
37599
39479
|
function registerSearchRoute(app, env) {
|
|
37600
39480
|
const { deps, globalSearchCapabilities, searchSync } = env;
|
|
37601
39481
|
app.post(
|
|
@@ -37610,8 +39490,7 @@ function registerSearchRoute(app, env) {
|
|
|
37610
39490
|
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
37611
39491
|
throw new TBError("invalid_argument", "body must be a JSON object");
|
|
37612
39492
|
}
|
|
37613
|
-
|
|
37614
|
-
if (bodyKeys.some((key) => key !== "query" && key !== "opts")) {
|
|
39493
|
+
if (Object.keys(body).some((key) => key !== "query" && key !== "opts")) {
|
|
37615
39494
|
throw new TBError("invalid_argument", "body only accepts query and opts");
|
|
37616
39495
|
}
|
|
37617
39496
|
if (typeof body.query !== "string" || body.query.trim().length === 0) {
|
|
@@ -37622,105 +39501,71 @@ function registerSearchRoute(app, env) {
|
|
|
37622
39501
|
throw new TBError("invalid_argument", "opts must be a JSON object");
|
|
37623
39502
|
}
|
|
37624
39503
|
const opts = rawOpts ?? {};
|
|
37625
|
-
|
|
37626
|
-
|
|
39504
|
+
const optionKeys = /* @__PURE__ */ new Set([
|
|
39505
|
+
"cursor",
|
|
39506
|
+
"detail",
|
|
39507
|
+
"effects",
|
|
39508
|
+
"federation",
|
|
39509
|
+
"limit",
|
|
39510
|
+
"matching",
|
|
39511
|
+
"minCoverage",
|
|
39512
|
+
"mode",
|
|
39513
|
+
"pathPrefix"
|
|
39514
|
+
]);
|
|
39515
|
+
if (Object.keys(opts).some((key) => !optionKeys.has(key))) {
|
|
39516
|
+
throw new TBError("invalid_argument", "opts contains an unknown search option");
|
|
37627
39517
|
}
|
|
37628
|
-
|
|
37629
|
-
|
|
37630
|
-
|
|
39518
|
+
if (opts.cursor !== void 0 && typeof opts.cursor !== "string") {
|
|
39519
|
+
throw new TBError("invalid_argument", "opts.cursor must be a string");
|
|
39520
|
+
}
|
|
39521
|
+
const parsed = toolSearchRequestSchema.safeParse({
|
|
39522
|
+
query: body.query,
|
|
39523
|
+
...rawOpts === void 0 ? {} : { opts }
|
|
39524
|
+
});
|
|
39525
|
+
if (!parsed.success) {
|
|
39526
|
+
throw new TBError(
|
|
39527
|
+
"invalid_argument",
|
|
39528
|
+
parsed.error.issues[0]?.message ?? "invalid search request"
|
|
39529
|
+
);
|
|
37631
39530
|
}
|
|
39531
|
+
const request = requestWithNormalizedLimit(parsed.data);
|
|
39532
|
+
const mode = request.opts?.mode ?? "keyword";
|
|
37632
39533
|
if (mode === "semantic" && !capabilities.includes("search:semantic")) {
|
|
37633
39534
|
throw new TBError(
|
|
37634
39535
|
"invalid_argument",
|
|
37635
39536
|
"search mode 'semantic' requires capability 'search:semantic'"
|
|
37636
39537
|
);
|
|
37637
39538
|
}
|
|
37638
|
-
const
|
|
37639
|
-
|
|
37640
|
-
|
|
37641
|
-
|
|
37642
|
-
|
|
37643
|
-
|
|
37644
|
-
|
|
37645
|
-
|
|
37646
|
-
const query = wireRequest.query;
|
|
37647
|
-
await searchSync?.ensureReady();
|
|
37648
|
-
const ctx = c.get("ctx");
|
|
37649
|
-
const registry = new NodeRegistryStore(c.get("store"));
|
|
37650
|
-
const selected = [];
|
|
37651
|
-
const workLimit = Math.min(
|
|
37652
|
-
TOOL_SEARCH_WORK_LIMIT,
|
|
37653
|
-
Math.max(TOOL_SEARCH_BATCH_LIMIT, limit * 2)
|
|
37654
|
-
);
|
|
37655
|
-
let scanCursor = opts.cursor;
|
|
37656
|
-
let responseCursor;
|
|
37657
|
-
let responseCandidate;
|
|
37658
|
-
let scanned = 0;
|
|
37659
|
-
let stopped = false;
|
|
37660
|
-
while (!stopped && selected.length < limit && scanned < workLimit) {
|
|
37661
|
-
const batchLimit = Math.min(TOOL_SEARCH_BATCH_LIMIT, workLimit - scanned);
|
|
37662
|
-
const page = await search.search(query, {
|
|
37663
|
-
mode,
|
|
37664
|
-
limit: batchLimit,
|
|
37665
|
-
...scanCursor === void 0 ? {} : { cursor: scanCursor }
|
|
37666
|
-
});
|
|
37667
|
-
if (page.items.length === 0) {
|
|
37668
|
-
responseCursor = page.cursor;
|
|
37669
|
-
break;
|
|
37670
|
-
}
|
|
37671
|
-
const nodes = await registry.getMany(page.items.map((candidate) => candidate.path));
|
|
37672
|
-
for (const [index, candidate] of page.items.entries()) {
|
|
37673
|
-
scanned++;
|
|
37674
|
-
if (!check(ctx, candidate.path, "read").allow || !check(ctx, candidate.path, "call").allow) {
|
|
37675
|
-
continue;
|
|
37676
|
-
}
|
|
37677
|
-
const node = nodes.get(candidate.path);
|
|
37678
|
-
if (node === void 0 || node.kind !== "mcp" && node.kind !== "http" && node.kind !== "tool" || node.config?.kind !== node.kind || virtualizeTools(node.virtualize, [{ name: candidate.name }]).exposed.length === 0) {
|
|
37679
|
-
continue;
|
|
37680
|
-
}
|
|
37681
|
-
selected.push({ candidate, node });
|
|
37682
|
-
if (selected.length === limit) {
|
|
37683
|
-
const hasUnscanned = index < page.items.length - 1 || page.cursor !== void 0;
|
|
37684
|
-
responseCandidate = hasUnscanned ? candidate : void 0;
|
|
37685
|
-
responseCursor = void 0;
|
|
37686
|
-
stopped = true;
|
|
37687
|
-
break;
|
|
37688
|
-
}
|
|
37689
|
-
}
|
|
37690
|
-
if (stopped) break;
|
|
37691
|
-
responseCursor = page.cursor;
|
|
37692
|
-
if (page.cursor === void 0) break;
|
|
37693
|
-
scanCursor = page.cursor;
|
|
37694
|
-
}
|
|
37695
|
-
const canonicalTools = await canonicalSearchTools(
|
|
37696
|
-
c.get("store"),
|
|
37697
|
-
selected.map((item) => item.node)
|
|
37698
|
-
);
|
|
37699
|
-
const items = [];
|
|
37700
|
-
let pageBytes = 0;
|
|
37701
|
-
for (const [index, selectedItem] of selected.entries()) {
|
|
37702
|
-
const raw = canonicalTools.get(selectedItem.candidate.path)?.find((tool2) => tool2.name === selectedItem.candidate.name);
|
|
37703
|
-
if (raw === void 0) continue;
|
|
37704
|
-
const tool = virtualizeTools(selectedItem.node.virtualize, [raw]).exposed[0];
|
|
37705
|
-
if (tool === void 0) continue;
|
|
37706
|
-
const item = { path: selectedItem.candidate.path, tool };
|
|
37707
|
-
const itemBytes = new TextEncoder().encode(JSON.stringify(item)).length;
|
|
37708
|
-
if (pageBytes + itemBytes > TOOL_SEARCH_PAGE_BYTES) {
|
|
37709
|
-
if (items.length === 0) {
|
|
37710
|
-
throw new TBError("internal", "\u5DE5\u5177\u641C\u7D22\u9875\u9762\u5B57\u8282\u9884\u7B97\u65E0\u6CD5\u5BB9\u7EB3\u9996\u4E2A\u7ED3\u679C");
|
|
37711
|
-
}
|
|
37712
|
-
responseCandidate = selected[index - 1]?.candidate;
|
|
37713
|
-
responseCursor = void 0;
|
|
37714
|
-
break;
|
|
37715
|
-
}
|
|
37716
|
-
items.push(item);
|
|
37717
|
-
pageBytes += itemBytes;
|
|
39539
|
+
const explicitFederation = request.opts?.federation;
|
|
39540
|
+
const cursor = request.opts?.cursor;
|
|
39541
|
+
const federation = explicitFederation ?? (cursor !== void 0 && !isFederatedSearchSessionHandle(cursor) ? "local" : capabilities.includes("search:federated") ? "recursive" : "local");
|
|
39542
|
+
if (federation === "recursive" && !capabilities.includes("search:federated")) {
|
|
39543
|
+
throw new TBError(
|
|
39544
|
+
"invalid_argument",
|
|
39545
|
+
"recursive search requires capability 'search:federated'"
|
|
39546
|
+
);
|
|
37718
39547
|
}
|
|
37719
|
-
if (
|
|
37720
|
-
|
|
39548
|
+
if (federation === "recursive" && mode === "semantic") {
|
|
39549
|
+
throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
|
|
37721
39550
|
}
|
|
37722
|
-
|
|
37723
|
-
|
|
39551
|
+
const normalizedRequest = {
|
|
39552
|
+
...request,
|
|
39553
|
+
opts: { ...request.opts ?? {}, federation }
|
|
39554
|
+
};
|
|
39555
|
+
const result = federation === "local" ? await executeLocalSearch({
|
|
39556
|
+
ctx: c.get("ctx"),
|
|
39557
|
+
search,
|
|
39558
|
+
...searchSync === void 0 ? {} : { searchSync },
|
|
39559
|
+
state: c.get("store")
|
|
39560
|
+
}, normalizedRequest) : await executeFederatedSearch(deps, {
|
|
39561
|
+
ctx: c.get("ctx"),
|
|
39562
|
+
headers: c.req.raw.headers,
|
|
39563
|
+
request: normalizedRequest,
|
|
39564
|
+
requestUrl: c.req.url,
|
|
39565
|
+
search,
|
|
39566
|
+
...searchSync === void 0 ? {} : { searchSync },
|
|
39567
|
+
state: c.get("store")
|
|
39568
|
+
});
|
|
37724
39569
|
return new Response(JSON.stringify(toolSearchPageSchema.parse(result)), {
|
|
37725
39570
|
headers: { "content-type": contentTypeFor("json") }
|
|
37726
39571
|
});
|
|
@@ -37909,7 +39754,7 @@ function mcpBridgeFor(c, env, app) {
|
|
|
37909
39754
|
invokePath: "/~search",
|
|
37910
39755
|
mcpName: "tb_search",
|
|
37911
39756
|
operation: "search",
|
|
37912
|
-
description: "Search visible tools across the Tool Bridge tree.",
|
|
39757
|
+
description: "Search visible tools across the Tool Bridge tree (compact by default).",
|
|
37913
39758
|
effect: "read",
|
|
37914
39759
|
inputSchema: {
|
|
37915
39760
|
type: "object",
|
|
@@ -37918,7 +39763,20 @@ function mcpBridgeFor(c, env, app) {
|
|
|
37918
39763
|
query: { type: "string", minLength: 1 },
|
|
37919
39764
|
mode: { type: "string", enum: ["keyword", "semantic"] },
|
|
37920
39765
|
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
37921
|
-
cursor: { type: "string", minLength: 1 }
|
|
39766
|
+
cursor: { type: "string", minLength: 1 },
|
|
39767
|
+
detail: { type: "string", enum: ["compact", "full"], default: "compact" },
|
|
39768
|
+
effects: {
|
|
39769
|
+
type: "array",
|
|
39770
|
+
minItems: 1,
|
|
39771
|
+
items: {
|
|
39772
|
+
type: "string",
|
|
39773
|
+
enum: ["read", "write", "destructive", "unknown"]
|
|
39774
|
+
}
|
|
39775
|
+
},
|
|
39776
|
+
federation: { type: "string", enum: ["local", "recursive"] },
|
|
39777
|
+
matching: { type: "string", enum: ["best", "all"] },
|
|
39778
|
+
minCoverage: { type: "number", exclusiveMinimum: 0, maximum: 1 },
|
|
39779
|
+
pathPrefix: { type: "string", minLength: 1 }
|
|
37922
39780
|
},
|
|
37923
39781
|
required: ["query"]
|
|
37924
39782
|
}
|
|
@@ -38126,15 +39984,27 @@ function mcpBridgeFor(c, env, app) {
|
|
|
38126
39984
|
if (tool.operation === "search") {
|
|
38127
39985
|
headers2.set("accept", "application/json");
|
|
38128
39986
|
headers2.set("content-type", "application/json");
|
|
38129
|
-
const opts =
|
|
38130
|
-
|
|
38131
|
-
|
|
39987
|
+
const opts = {
|
|
39988
|
+
detail: args.detail ?? "compact",
|
|
39989
|
+
...Object.fromEntries(
|
|
39990
|
+
[
|
|
39991
|
+
"mode",
|
|
39992
|
+
"limit",
|
|
39993
|
+
"cursor",
|
|
39994
|
+
"effects",
|
|
39995
|
+
"federation",
|
|
39996
|
+
"matching",
|
|
39997
|
+
"minCoverage",
|
|
39998
|
+
"pathPrefix"
|
|
39999
|
+
].filter((key) => args[key] !== void 0).map((key) => [key, args[key]])
|
|
40000
|
+
)
|
|
40001
|
+
};
|
|
38132
40002
|
const response2 = await app.request(new Request(new URL("/~search", c.req.url), {
|
|
38133
40003
|
method: "POST",
|
|
38134
40004
|
headers: headers2,
|
|
38135
40005
|
body: JSON.stringify({
|
|
38136
40006
|
query: args.query,
|
|
38137
|
-
|
|
40007
|
+
opts
|
|
38138
40008
|
})
|
|
38139
40009
|
}));
|
|
38140
40010
|
return await resultFromResponse(response2);
|
|
@@ -38395,6 +40265,7 @@ async function handleInvoke(c, env) {
|
|
|
38395
40265
|
deps
|
|
38396
40266
|
);
|
|
38397
40267
|
assertSecretRefUse(ctx.scopes, cfgPatch);
|
|
40268
|
+
await assertMcpOAuthConfig(cfgPatch, deps.secrets);
|
|
38398
40269
|
await assertContextConfig(cfgPatch, deps);
|
|
38399
40270
|
await assertSkillhubConfig(cfgPatch, deps);
|
|
38400
40271
|
await assertToolConfig(cfgPatch, deps, ctx, targetPath);
|
|
@@ -38490,6 +40361,7 @@ function createRouteEnv(deps) {
|
|
|
38490
40361
|
if (!declared.has("search")) return [];
|
|
38491
40362
|
return [
|
|
38492
40363
|
"search",
|
|
40364
|
+
...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : [],
|
|
38493
40365
|
...declared.has("search:semantic") ? ["search:semantic"] : []
|
|
38494
40366
|
];
|
|
38495
40367
|
};
|
|
@@ -38526,8 +40398,17 @@ async function handleHelp(c, env) {
|
|
|
38526
40398
|
return renderHelp(await enrichHelp(model2, "", store), rep);
|
|
38527
40399
|
}
|
|
38528
40400
|
if (!check(ctx, path, "read").allow) throw TBError.notFound("not found");
|
|
38529
|
-
const
|
|
38530
|
-
if (
|
|
40401
|
+
const projector = await remotePathProjectorIfMatch(registry, path);
|
|
40402
|
+
if (projector !== null) {
|
|
40403
|
+
const headers = new Headers(c.req.raw.headers);
|
|
40404
|
+
headers.set("accept", "application/json");
|
|
40405
|
+
const remote = await remotePassthroughIfMatch(c, ctx, registry, path, "~help", deps, headers);
|
|
40406
|
+
if (remote === null) throw remoteProtocolError("remote ~help lost its mount owner");
|
|
40407
|
+
if (!remote.ok) return remote;
|
|
40408
|
+
const parsed = helpJsonSchema.safeParse(await remote.json().catch(() => null));
|
|
40409
|
+
if (!parsed.success) throw remoteProtocolError("remote ~help returned invalid JSON");
|
|
40410
|
+
return renderHelp(projector.projectHelp(parsed.data, path, ctx), rep);
|
|
40411
|
+
}
|
|
38531
40412
|
const builtins = builtinsOf(store);
|
|
38532
40413
|
const refresh = c.req.query("refresh") === "1";
|
|
38533
40414
|
const schemas2 = c.req.query("schemas") === "1";
|
|
@@ -38554,18 +40435,45 @@ async function handleHelp(c, env) {
|
|
|
38554
40435
|
|
|
38555
40436
|
// ../app/src/routes/tree.ts
|
|
38556
40437
|
var REMOTE_OPAQUE_KINDS = /* @__PURE__ */ new Set(["remote"]);
|
|
40438
|
+
function renderTree(tree, rep) {
|
|
40439
|
+
if (rep === "json") {
|
|
40440
|
+
return new Response(JSON.stringify(treeJsonSchema.parse(tree)), {
|
|
40441
|
+
headers: { "content-type": contentTypeFor("json") }
|
|
40442
|
+
});
|
|
40443
|
+
}
|
|
40444
|
+
if (rep === "dsl") {
|
|
40445
|
+
return new Response(renderTreeDsl(tree), {
|
|
40446
|
+
headers: { "content-type": contentTypeFor("dsl") }
|
|
40447
|
+
});
|
|
40448
|
+
}
|
|
40449
|
+
return new Response(`\`\`\`text
|
|
40450
|
+
${renderTreeDsl(tree)}\`\`\`
|
|
40451
|
+
`, {
|
|
40452
|
+
headers: { "content-type": contentTypeFor("markdown") }
|
|
40453
|
+
});
|
|
40454
|
+
}
|
|
38557
40455
|
async function handleTree(c, env) {
|
|
38558
40456
|
const { deps } = env;
|
|
38559
40457
|
const path = splitReserved(new URL(c.req.url).pathname, "~tree");
|
|
38560
40458
|
if (path === null) throw TBError.notFound("no such path");
|
|
38561
40459
|
const ctx = c.get("ctx");
|
|
38562
40460
|
const store = c.get("store");
|
|
40461
|
+
const rep = negotiate(c.req.header("accept"));
|
|
38563
40462
|
if (path !== "" && !check(ctx, path, "read").allow) throw TBError.notFound("not found");
|
|
38564
40463
|
const registry = new NodeRegistryStore(store);
|
|
38565
40464
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
38566
40465
|
if (path !== "") {
|
|
38567
|
-
const
|
|
38568
|
-
if (
|
|
40466
|
+
const projector = await remotePathProjectorIfMatch(registry, path);
|
|
40467
|
+
if (projector !== null) {
|
|
40468
|
+
const headers = new Headers(c.req.raw.headers);
|
|
40469
|
+
headers.set("accept", "application/json");
|
|
40470
|
+
const remote = await remotePassthroughIfMatch(c, ctx, registry, path, "~tree", deps, headers);
|
|
40471
|
+
if (remote === null) throw remoteProtocolError("remote ~tree lost its mount owner");
|
|
40472
|
+
if (!remote.ok) return remote;
|
|
40473
|
+
const parsed = treeJsonSchema.safeParse(await remote.json().catch(() => null));
|
|
40474
|
+
if (!parsed.success) throw remoteProtocolError("remote ~tree returned invalid JSON");
|
|
40475
|
+
return renderTree(projector.projectTree(parsed.data, path, ctx), rep);
|
|
40476
|
+
}
|
|
38569
40477
|
}
|
|
38570
40478
|
let rootEntry;
|
|
38571
40479
|
if (path !== "") {
|
|
@@ -38596,22 +40504,7 @@ async function handleTree(c, env) {
|
|
|
38596
40504
|
opaqueKinds: REMOTE_OPAQUE_KINDS,
|
|
38597
40505
|
...rootEntry !== void 0 ? { rootEntry } : {}
|
|
38598
40506
|
});
|
|
38599
|
-
|
|
38600
|
-
if (rep === "json") {
|
|
38601
|
-
return new Response(JSON.stringify(treeJsonSchema.parse(tree)), {
|
|
38602
|
-
headers: { "content-type": contentTypeFor("json") }
|
|
38603
|
-
});
|
|
38604
|
-
}
|
|
38605
|
-
if (rep === "dsl") {
|
|
38606
|
-
return new Response(renderTreeDsl(tree), {
|
|
38607
|
-
headers: { "content-type": contentTypeFor("dsl") }
|
|
38608
|
-
});
|
|
38609
|
-
}
|
|
38610
|
-
return new Response(`\`\`\`text
|
|
38611
|
-
${renderTreeDsl(tree)}\`\`\`
|
|
38612
|
-
`, {
|
|
38613
|
-
headers: { "content-type": contentTypeFor("markdown") }
|
|
38614
|
-
});
|
|
40507
|
+
return renderTree(tree, rep);
|
|
38615
40508
|
}
|
|
38616
40509
|
|
|
38617
40510
|
// ../app/src/tbApp.ts
|
|
@@ -39414,6 +41307,40 @@ var FsObjectStore = class {
|
|
|
39414
41307
|
// ../core/src/node/shellExecutor.ts
|
|
39415
41308
|
import { spawn as nodeSpawn } from "child_process";
|
|
39416
41309
|
var SHELL_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
41310
|
+
var SHELL_EXEC_DEFAULT_TIMEOUT_MS = 55e3;
|
|
41311
|
+
|
|
41312
|
+
// ../core/src/node/structuredCommand.ts
|
|
41313
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
41314
|
+
var STRUCTURED_COMMAND_PROFILE_VERSION = 1;
|
|
41315
|
+
var STRUCTURED_COMMAND_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
41316
|
+
var envNameSchema = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
41317
|
+
var argumentSchema = external_exports.strictObject({
|
|
41318
|
+
input: external_exports.string().min(1),
|
|
41319
|
+
description: external_exports.string().min(1).optional(),
|
|
41320
|
+
flag: external_exports.string().min(1).optional(),
|
|
41321
|
+
choices: external_exports.array(external_exports.string()).min(1).optional(),
|
|
41322
|
+
multiple: external_exports.boolean().optional(),
|
|
41323
|
+
required: external_exports.boolean().optional(),
|
|
41324
|
+
type: external_exports.enum(["string", "number", "boolean"]).optional()
|
|
41325
|
+
});
|
|
41326
|
+
var commandSchema = external_exports.strictObject({
|
|
41327
|
+
name: external_exports.string().min(1),
|
|
41328
|
+
description: external_exports.string().min(1),
|
|
41329
|
+
executable: external_exports.string().min(1),
|
|
41330
|
+
argv: external_exports.array(external_exports.union([external_exports.string(), argumentSchema])).optional(),
|
|
41331
|
+
cwd: external_exports.string().min(1).optional(),
|
|
41332
|
+
effect: external_exports.enum(["read", "write", "destructive"]),
|
|
41333
|
+
confirm: external_exports.boolean().optional(),
|
|
41334
|
+
timeoutMs: external_exports.number().int().positive().max(SHELL_EXEC_DEFAULT_TIMEOUT_MS).optional(),
|
|
41335
|
+
maxOutputBytes: external_exports.number().int().positive().max(STRUCTURED_COMMAND_MAX_OUTPUT_BYTES).optional(),
|
|
41336
|
+
inheritEnv: external_exports.array(envNameSchema).optional()
|
|
41337
|
+
});
|
|
41338
|
+
var profileSchema = external_exports.strictObject({
|
|
41339
|
+
version: external_exports.literal(STRUCTURED_COMMAND_PROFILE_VERSION),
|
|
41340
|
+
path: external_exports.string().min(1),
|
|
41341
|
+
description: external_exports.string().min(1),
|
|
41342
|
+
commands: external_exports.array(commandSchema).min(1)
|
|
41343
|
+
});
|
|
39417
41344
|
|
|
39418
41345
|
// src/objects.ts
|
|
39419
41346
|
import { dirname as dirname2, join as join2, sep as sep2 } from "path";
|
|
@@ -39697,7 +41624,7 @@ var PgSearchDriver = class {
|
|
|
39697
41624
|
records,
|
|
39698
41625
|
PG_SEARCH_TOOLS_TABLE,
|
|
39699
41626
|
PG_SEARCH_TOOLS_COLUMNS,
|
|
39700
|
-
(record2) => [record2.path, record2.name, record2.description, record2.feedback]
|
|
41627
|
+
(record2) => [record2.path, record2.name, record2.description, record2.effect, record2.feedback]
|
|
39701
41628
|
);
|
|
39702
41629
|
}
|
|
39703
41630
|
insertSnapshots(digests) {
|
|
@@ -39887,7 +41814,7 @@ var SqliteSearchDriver = class {
|
|
|
39887
41814
|
}
|
|
39888
41815
|
insertRecords(records) {
|
|
39889
41816
|
return records.map((record2) => ({
|
|
39890
|
-
params: [record2.path, record2.name, record2.description, record2.feedback],
|
|
41817
|
+
params: [record2.path, record2.name, record2.description, record2.effect, record2.feedback],
|
|
39891
41818
|
sql: TOOL_SEARCH_INSERT_SQL
|
|
39892
41819
|
}));
|
|
39893
41820
|
}
|
|
@@ -40031,7 +41958,7 @@ import { join as join4 } from "path";
|
|
|
40031
41958
|
// package.json
|
|
40032
41959
|
var package_default = {
|
|
40033
41960
|
name: "@tool-bridge/server",
|
|
40034
|
-
version: "0.
|
|
41961
|
+
version: "0.19.0",
|
|
40035
41962
|
description: "tool-bridge self-hosted server: run the HTBP gateway on Node with SQLite or PostgreSQL persistence, filesystem object storage and WebSocket device channel \u2014 single container via docker run",
|
|
40036
41963
|
type: "module",
|
|
40037
41964
|
license: "MIT",
|