@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.
Files changed (108) hide show
  1. package/dist/{airtable-NOSOMOVB.js → airtable-23JBIM6X.js} +4 -4
  2. package/dist/{alt_text_generator_ai-BHIJ4LW5.js → alt_text_generator_ai-ADWBTCIH.js} +4 -4
  3. package/dist/{amap-QRLP4TWZ.js → amap-CYQCYW2Z.js} +4 -4
  4. package/dist/{anthropic-IEOI36EV.js → anthropic-5P62HDNR.js} +4 -4
  5. package/dist/{apify-3CCYTQVK.js → apify-54RZUAGW.js} +4 -4
  6. package/dist/{brave_search-PZNYDQG7.js → brave_search-AHJ6FTD5.js} +4 -4
  7. package/dist/{chunk-YASUMV2K.js → chunk-B4P6YZFS.js} +1 -1
  8. package/dist/{chunk-TLNUIPFZ.js → chunk-CLEZ6YQ6.js} +2117 -190
  9. package/dist/{chunk-RVWZN3CI.js → chunk-OKRZPQ3R.js} +443 -139
  10. package/dist/{chunk-U5UHT7CX.js → chunk-SWBNHIO4.js} +1 -1
  11. package/dist/{chunk-WW2IMPQK.js → chunk-VLOIDU5U.js} +3 -3
  12. package/dist/{circleci-VSJ4O5FC.js → circleci-UM3NSQJ3.js} +4 -4
  13. package/dist/{clerk-CC2JZBZI.js → clerk-FE5LQ2CZ.js} +4 -4
  14. package/dist/{cloudflare_dns-PJEBSEYQ.js → cloudflare_dns-KEZABKUT.js} +4 -4
  15. package/dist/{cohere-KBACYSPF.js → cohere-4IPJADXP.js} +4 -4
  16. package/dist/{confluence-2PX3O7G5.js → confluence-56QEKNQI.js} +4 -4
  17. package/dist/{convertapi-BVOTD2PJ.js → convertapi-3QRM5K5T.js} +3 -3
  18. package/dist/{currencyapi-6ZQ2VNQ7.js → currencyapi-L6G4OND5.js} +4 -4
  19. package/dist/{deepseek-MB3WVVTB.js → deepseek-LGC6HBFG.js} +4 -4
  20. package/dist/{docker_hub-7CLQL2IJ.js → docker_hub-XDMK3BIU.js} +4 -4
  21. package/dist/{dropbox-VFYDVUNK.js → dropbox-4QARMFZU.js} +4 -4
  22. package/dist/{dub-QN3EETJY.js → dub-C6JSJKOT.js} +4 -4
  23. package/dist/{e2b-NKC3Y776.js → e2b-6A6TVACJ.js} +4 -4
  24. package/dist/{exa-KKZ6F25K.js → exa-SIWWW6AA.js} +4 -4
  25. package/dist/{fal_ai-25LYJD2O.js → fal_ai-3IJXOKSP.js} +4 -4
  26. package/dist/{feishu-NRT55B3Q.js → feishu-L4KDA4DC.js} +2 -2
  27. package/dist/{feishu_custom_bot-3W3XXUG4.js → feishu_custom_bot-SDM75H4P.js} +3 -3
  28. package/dist/{figma-4FLP35TC.js → figma-6DD436DN.js} +4 -4
  29. package/dist/{firehydrant-BLXPLZ3X.js → firehydrant-SYSGYB3R.js} +4 -4
  30. package/dist/{fixer-4OR62TGE.js → fixer-FZG7ASTF.js} +4 -4
  31. package/dist/{fly-MYCKOZZK.js → fly-HMRCZBUC.js} +4 -4
  32. package/dist/{front-3DFYVMF3.js → front-FAFSD7I3.js} +4 -4
  33. package/dist/{geocodio-CXMD6VKA.js → geocodio-4D3TWENM.js} +4 -4
  34. package/dist/{ghost-4UUE737W.js → ghost-OSO2FOZS.js} +4 -4
  35. package/dist/{github-WFS2ON2N.js → github-OR4I4RHE.js} +4 -4
  36. package/dist/{gmail-KW7FXA7A.js → gmail-5AOPJ63E.js} +4 -4
  37. package/dist/{googlecalendar-AMH5JQMC.js → googlecalendar-SOJIJUWH.js} +4 -4
  38. package/dist/{googledocs-R5VA6CKD.js → googledocs-QSKN5TLN.js} +3 -3
  39. package/dist/{grafana-IT6DWIVB.js → grafana-R3AIZM7M.js} +4 -4
  40. package/dist/{graphhopper-TYFDJZWC.js → graphhopper-4CR3QOAX.js} +4 -4
  41. package/dist/index.d.ts +53 -8
  42. package/dist/index.js +3 -3
  43. package/dist/{ip2proxy-GJ4HXVXO.js → ip2proxy-OBCG6X6J.js} +4 -4
  44. package/dist/{ipgeolocation_io-OAGR6PRF.js → ipgeolocation_io-NHH3DTQW.js} +4 -4
  45. package/dist/{ipqualityscore-BCKAMDGL.js → ipqualityscore-7POMCBEC.js} +4 -4
  46. package/dist/{jina_ai-N3NXRVIZ.js → jina_ai-YXVKROV4.js} +4 -4
  47. package/dist/{jira-CHLBKWLO.js → jira-LADO3VJU.js} +4 -4
  48. package/dist/{langsmith-OIKOTPY3.js → langsmith-XZ6UAQFU.js} +4 -4
  49. package/dist/{lemlist-VAKYPKS3.js → lemlist-P74GLNE7.js} +4 -4
  50. package/dist/{linear-Q6QTPJ5R.js → linear-CXJ7D37A.js} +3 -3
  51. package/dist/{logsnag-SQOQBTES.js → logsnag-EVVHWFLK.js} +4 -4
  52. package/dist/main.js +102 -102
  53. package/dist/{mattermost-G2KBEICM.js → mattermost-WG2MRGBD.js} +4 -4
  54. package/dist/{mem0-D2FPLYDX.js → mem0-HQWUKG6U.js} +4 -4
  55. package/dist/{memos-76PTWV5V.js → memos-SQMJIFOE.js} +4 -4
  56. package/dist/{metabase-TWSIDVBM.js → metabase-E27G2DZI.js} +4 -4
  57. package/dist/{mistral_ai-TP7TQUEJ.js → mistral_ai-MIL63NIT.js} +4 -4
  58. package/dist/{mother_duck-UXY5THYU.js → mother_duck-AIZVCBNR.js} +4 -4
  59. package/dist/{ngrok-RZW2ISJK.js → ngrok-XS4DVIQS.js} +4 -4
  60. package/dist/{notes-B2I6FWK2.js → notes-JEOUSL2Y.js} +1 -1
  61. package/dist/{notion-QOKRUE6M.js → notion-U4EZVDW2.js} +4 -4
  62. package/dist/{open_exchange_rates-ODLFWEJ7.js → open_exchange_rates-AZSQEYEJ.js} +4 -4
  63. package/dist/{openai-ZPBPPPGT.js → openai-HW3SYMTQ.js} +4 -4
  64. package/dist/{opengraph_io-EOI5UJKT.js → opengraph_io-QX74MAEL.js} +4 -4
  65. package/dist/{openrouter-BUWLF3BG.js → openrouter-OXSKYL6N.js} +4 -4
  66. package/dist/{outline-NXS3QRXO.js → outline-CCQAPDSW.js} +4 -4
  67. package/dist/{paddle-T4MSKQ76.js → paddle-6KQPSSCK.js} +4 -4
  68. package/dist/{perplexity-WW5N2LW7.js → perplexity-2TXO7IQE.js} +4 -4
  69. package/dist/{pinecone-RY5AZPQM.js → pinecone-YOO7NULI.js} +4 -4
  70. package/dist/{pipedrive-OHYQNZTU.js → pipedrive-Q6RIFIFP.js} +4 -4
  71. package/dist/{polar-XZIAHXS5.js → polar-KUMGCES3.js} +4 -4
  72. package/dist/{posthog-DTPTWORY.js → posthog-VXQHBSMB.js} +4 -4
  73. package/dist/{postmark-F4UCQKC3.js → postmark-YLIPF52S.js} +4 -4
  74. package/dist/{prerender-P3Y2MLIC.js → prerender-CRY7UJN5.js} +4 -4
  75. package/dist/{pubmed-RHU3GXFB.js → pubmed-3Z4T5AM7.js} +3 -3
  76. package/dist/{railway-VB6SRLI6.js → railway-BP37IGFL.js} +3 -3
  77. package/dist/{readwise-DIMSKJZR.js → readwise-HTKICF4K.js} +4 -4
  78. package/dist/{render-XCCDCEEK.js → render-WIVTUXJH.js} +4 -4
  79. package/dist/{replicate-IUEOQ4JC.js → replicate-JJYPA43D.js} +4 -4
  80. package/dist/{resend-N5MJZH5E.js → resend-TKHZVG7A.js} +4 -4
  81. package/dist/{rootly-25T4NBTU.js → rootly-FRSZLG5U.js} +4 -4
  82. package/dist/{scrapfly-PK4XFXUA.js → scrapfly-M5WXGQWE.js} +4 -4
  83. package/dist/{scrapingbee-ITPBMWQU.js → scrapingbee-PMXNLLBK.js} +4 -4
  84. package/dist/{screenshot_fyi-5U6ZYPKR.js → screenshot_fyi-HFW4TAOH.js} +4 -4
  85. package/dist/{semantic_scholar-BJGCI2MU.js → semantic_scholar-KXVH2VZQ.js} +4 -4
  86. package/dist/{sentry-63IC263P.js → sentry-WWOT2LRX.js} +4 -4
  87. package/dist/{shodan-2WBIFHFN.js → shodan-FY635H35.js} +4 -4
  88. package/dist/{shopify-XLLJRAGM.js → shopify-HQLBVSA5.js} +4 -4
  89. package/dist/{stripe-WLVCY3UN.js → stripe-GJN2GOHB.js} +3 -3
  90. package/dist/{supabase-BKNZ35ER.js → supabase-BBNZGQU6.js} +4 -4
  91. package/dist/{tavily-YFSDEUTL.js → tavily-DPENIBOG.js} +4 -4
  92. package/dist/{telegram-FC35W74G.js → telegram-L4NONG6H.js} +4 -4
  93. package/dist/{telnyx-MI32XZUG.js → telnyx-H67MAEZS.js} +4 -4
  94. package/dist/{todoist-4ZFJKZC6.js → todoist-G2BSP4DX.js} +4 -4
  95. package/dist/{trello-UOGFNMK4.js → trello-7WMVFDG2.js} +4 -4
  96. package/dist/{turso-YTSTW3ZS.js → turso-ZUAESFC2.js} +4 -4
  97. package/dist/{twilio-XAJDFUQH.js → twilio-EYFYIX3B.js} +3 -3
  98. package/dist/{umami-ZVIUBPAI.js → umami-AXAUDUVF.js} +4 -4
  99. package/dist/{unsplash-IU3PLDNS.js → unsplash-UCQAWPLR.js} +4 -4
  100. package/dist/{upstash_redis-3EYJW7SY.js → upstash_redis-ZPZ4ASU6.js} +4 -4
  101. package/dist/{uptimerobot-PJG2ASRU.js → uptimerobot-4QVKKUD5.js} +3 -3
  102. package/dist/{v2ex-KFQWWGFC.js → v2ex-VGEFJTYC.js} +4 -4
  103. package/dist/{vercel-I2ZDOQRL.js → vercel-JME4TSCN.js} +4 -4
  104. package/dist/{wolfram_alpha_api-BNXOJMB6.js → wolfram_alpha_api-TRGU2MDG.js} +4 -4
  105. package/dist/{wordpress-36D7BSMP.js → wordpress-WRUKLCZR.js} +4 -4
  106. package/dist/{workos-6X4JJRRC.js → workos-ZDW4EHS4.js} +4 -4
  107. package/dist/{zhihu-CIXKCHIU.js → zhihu-4VI67CWU.js} +4 -4
  108. package/package.json +5 -5
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createGuardedFetch
3
- } from "./chunk-U5UHT7CX.js";
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-RVWZN3CI.js";
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 consume = consumeHextets;
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 (!consume(buffer, address, output)) break;
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 (!consume(buffer, address, output)) break;
10042
- consume = consumeIsZone;
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 (consume === consumeIsZone) output.zone = buffer.join("");
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
- token_endpoint_auth_method: "none"
19256
- // public client,PKCE 保护
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
- return raw === null ? void 0 : raw;
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.opts.store.put(KEY_OAUTH_TOKENS + this.opts.nodePath, tokens);
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
- ...this.opts.redirectUri !== void 0 ? { r: this.opts.redirectUri } : {}
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") await store.delete(KEY_OAUTH_CLIENT + nodePath);
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
- assertLocalRedirectUri(opts.redirectUri);
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(opts.redirectUri)) {
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 fetch(input, init);
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 consume = consumeHextets;
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 (!consume(buffer, address, output)) break;
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 (!consume(buffer, address, output)) break;
28176
- consume = consumeIsZone;
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 (consume === consumeIsZone) output.zone = buffer.join("");
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 resp.text();
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, { status: resp.status, headers: { "content-type": respCt } });
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
- let decoded = segment;
36994
- for (let pass = 0; pass < 4; pass++) {
36995
- if (decoded === "." || decoded === ".." || decoded.startsWith("~") || decoded.includes("/") || decoded.includes("\\") || [...decoded].some((char) => {
36996
- const code = char.charCodeAt(0);
36997
- return code <= 31 || code === 127;
36998
- })) {
36999
- throw remoteProtocolError(`remote returned unsafe path segment '${segment}'`);
37000
- }
37001
- let next;
37002
- try {
37003
- next = decodeURIComponent(decoded);
37004
- } catch {
37005
- throw remoteProtocolError(`remote returned malformed encoded path '${path}'`);
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
- function localizeRemoteEntry(mountPath, remoteParentPath, entry) {
37020
- const rel = canonicalRemotePath(entry.path, false);
37021
- const parent = rel.split("/").slice(0, -1).join("/");
37022
- if (parent !== remoteParentPath) {
37023
- throw remoteProtocolError(`remote ~tree child '${rel}' is not a direct descendant`);
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
- const out = {
37026
- path: `${mountPath}/${rel}`,
37027
- kind: entry.kind,
37028
- description: entry.description
37029
- };
37030
- if (entry.presence !== void 0) out.presence = entry.presence;
37031
- return out;
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 remoteTree = await resp.json().catch(() => null);
37086
- if (remoteTree === null) {
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 remotePath = canonicalRemotePath(remoteTree.path, true);
37090
- if (remotePath !== resolved.rest) {
37091
- throw remoteProtocolError(`remote ~tree path '${remotePath}' does not match request`);
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
- const bodyKeys = Object.keys(body);
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
- if (Object.keys(opts).some((key) => key !== "mode" && key !== "limit" && key !== "cursor")) {
37626
- throw new TBError("invalid_argument", "opts only accepts mode, limit and cursor");
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
- const mode = opts.mode ?? "keyword";
37629
- if (mode !== "keyword" && mode !== "semantic") {
37630
- throw new TBError("invalid_argument", "opts.mode must be 'keyword' or 'semantic'");
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 limit = normalizeToolSearchLimit(opts.limit);
37639
- if (opts.cursor !== void 0 && typeof opts.cursor !== "string") {
37640
- throw new TBError("invalid_argument", "opts.cursor must be a string");
37641
- }
37642
- const wireRequest = toolSearchRequestSchema.parse({
37643
- query: body.query,
37644
- ...rawOpts === void 0 ? {} : { opts }
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 (responseCandidate !== void 0) {
37720
- responseCursor = await search.cursorFor(query, responseCandidate, mode);
39548
+ if (federation === "recursive" && mode === "semantic") {
39549
+ throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
37721
39550
  }
37722
- if (items.length === 0) responseCursor = void 0;
37723
- const result = responseCursor === void 0 ? { items } : { items, cursor: responseCursor };
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 = Object.fromEntries(
38130
- ["mode", "limit", "cursor"].filter((key) => args[key] !== void 0).map((key) => [key, args[key]])
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
- ...Object.keys(opts).length === 0 ? {} : { opts }
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 remote = await remotePassthroughIfMatch(c, ctx, registry, path, "~help", deps);
38530
- if (remote) return remote;
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 remote = await remotePassthroughIfMatch(c, ctx, registry, path, "~tree", deps);
38568
- if (remote) return remote;
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
- const rep = negotiate(c.req.header("accept"));
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.17.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",