@tool-bridge/server 0.17.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{airtable-NOSOMOVB.js → airtable-ZNWHIJMO.js} +4 -4
- package/dist/{alt_text_generator_ai-BHIJ4LW5.js → alt_text_generator_ai-X7RMDDM5.js} +4 -4
- package/dist/{amap-QRLP4TWZ.js → amap-3VER75VH.js} +4 -4
- package/dist/{anthropic-IEOI36EV.js → anthropic-N7NTB3UC.js} +4 -4
- package/dist/{apify-3CCYTQVK.js → apify-JB5P5AJI.js} +4 -4
- package/dist/{brave_search-PZNYDQG7.js → brave_search-USL5OOBS.js} +4 -4
- package/dist/{chunk-U5UHT7CX.js → chunk-3WL6EXYX.js} +1 -1
- package/dist/{chunk-YASUMV2K.js → chunk-6ANLYNFB.js} +1 -1
- package/dist/{chunk-WW2IMPQK.js → chunk-6X55ALDX.js} +3 -3
- package/dist/{chunk-TLNUIPFZ.js → chunk-RVUERWUZ.js} +2698 -247
- package/dist/{chunk-RVWZN3CI.js → chunk-WIGQCP6O.js} +1306 -207
- package/dist/{circleci-VSJ4O5FC.js → circleci-LBGB25LP.js} +4 -4
- package/dist/{clerk-CC2JZBZI.js → clerk-6RX37CNZ.js} +4 -4
- package/dist/{cloudflare_dns-PJEBSEYQ.js → cloudflare_dns-NQGW7FHV.js} +4 -4
- package/dist/{cohere-KBACYSPF.js → cohere-XPC2NAEW.js} +4 -4
- package/dist/{confluence-2PX3O7G5.js → confluence-ZUG2LT5A.js} +4 -4
- package/dist/{convertapi-BVOTD2PJ.js → convertapi-ZLXKWV3L.js} +3 -3
- package/dist/{currencyapi-6ZQ2VNQ7.js → currencyapi-T3A3HSOD.js} +4 -4
- package/dist/{deepseek-MB3WVVTB.js → deepseek-NJMOD23E.js} +4 -4
- package/dist/{docker_hub-7CLQL2IJ.js → docker_hub-D2MQCSJ7.js} +4 -4
- package/dist/{dropbox-VFYDVUNK.js → dropbox-MI6U3SOT.js} +4 -4
- package/dist/{dub-QN3EETJY.js → dub-P2BYBYV4.js} +4 -4
- package/dist/{e2b-NKC3Y776.js → e2b-BGKIAE54.js} +4 -4
- package/dist/{exa-KKZ6F25K.js → exa-EUY7VV4C.js} +4 -4
- package/dist/{fal_ai-25LYJD2O.js → fal_ai-AAOG5FYJ.js} +4 -4
- package/dist/{feishu-NRT55B3Q.js → feishu-AN5IJYOW.js} +2 -2
- package/dist/{feishu_custom_bot-3W3XXUG4.js → feishu_custom_bot-BWGMNVXF.js} +3 -3
- package/dist/{figma-4FLP35TC.js → figma-SJSWYCGD.js} +4 -4
- package/dist/{firehydrant-BLXPLZ3X.js → firehydrant-MOXK2PST.js} +4 -4
- package/dist/{fixer-4OR62TGE.js → fixer-ISIDUDZU.js} +4 -4
- package/dist/{fly-MYCKOZZK.js → fly-U4KPSGBF.js} +4 -4
- package/dist/{front-3DFYVMF3.js → front-ZHM2YX2E.js} +4 -4
- package/dist/{geocodio-CXMD6VKA.js → geocodio-SVCCAXAV.js} +4 -4
- package/dist/{ghost-4UUE737W.js → ghost-INTR7FIY.js} +4 -4
- package/dist/{github-WFS2ON2N.js → github-T275VVWR.js} +4 -4
- package/dist/{gmail-KW7FXA7A.js → gmail-KSSPOE67.js} +4 -4
- package/dist/{googlecalendar-AMH5JQMC.js → googlecalendar-JB5RW27L.js} +4 -4
- package/dist/{googledocs-R5VA6CKD.js → googledocs-G522PUGW.js} +3 -3
- package/dist/{grafana-IT6DWIVB.js → grafana-44ZPIV5P.js} +4 -4
- package/dist/{graphhopper-TYFDJZWC.js → graphhopper-2MORXLOW.js} +4 -4
- package/dist/index.d.ts +74 -14
- package/dist/index.js +3 -3
- package/dist/{ip2proxy-GJ4HXVXO.js → ip2proxy-R4K4KEMI.js} +4 -4
- package/dist/{ipgeolocation_io-OAGR6PRF.js → ipgeolocation_io-H4J55OGI.js} +4 -4
- package/dist/{ipqualityscore-BCKAMDGL.js → ipqualityscore-XGID4OHA.js} +4 -4
- package/dist/{jina_ai-N3NXRVIZ.js → jina_ai-7ZVDOR3Z.js} +4 -4
- package/dist/{jira-CHLBKWLO.js → jira-CTIYZTT3.js} +4 -4
- package/dist/{langsmith-OIKOTPY3.js → langsmith-ISP567FS.js} +4 -4
- package/dist/{lemlist-VAKYPKS3.js → lemlist-OGKK272F.js} +4 -4
- package/dist/{linear-Q6QTPJ5R.js → linear-6JX4E3JI.js} +3 -3
- package/dist/{logsnag-SQOQBTES.js → logsnag-BVB36ZCL.js} +4 -4
- package/dist/main.js +102 -102
- package/dist/{mattermost-G2KBEICM.js → mattermost-UECVPZGY.js} +4 -4
- package/dist/{mem0-D2FPLYDX.js → mem0-EUA7S2OO.js} +4 -4
- package/dist/{memos-76PTWV5V.js → memos-BYSBJRWZ.js} +4 -4
- package/dist/{metabase-TWSIDVBM.js → metabase-4JXZH3YG.js} +4 -4
- package/dist/{mistral_ai-TP7TQUEJ.js → mistral_ai-2C5BTBKW.js} +4 -4
- package/dist/{mother_duck-UXY5THYU.js → mother_duck-ZGNNNR3X.js} +4 -4
- package/dist/{ngrok-RZW2ISJK.js → ngrok-NTPGMRXI.js} +4 -4
- package/dist/{notes-B2I6FWK2.js → notes-KGYXX6N6.js} +1 -1
- package/dist/{notion-QOKRUE6M.js → notion-EX3X2G7G.js} +4 -4
- package/dist/{open_exchange_rates-ODLFWEJ7.js → open_exchange_rates-2V2LIGPS.js} +4 -4
- package/dist/{openai-ZPBPPPGT.js → openai-KQFQPK7Z.js} +4 -4
- package/dist/{opengraph_io-EOI5UJKT.js → opengraph_io-B63SEYJY.js} +4 -4
- package/dist/{openrouter-BUWLF3BG.js → openrouter-62A4H62Q.js} +4 -4
- package/dist/{outline-NXS3QRXO.js → outline-QC33XLCK.js} +4 -4
- package/dist/{paddle-T4MSKQ76.js → paddle-HDZG35Y7.js} +4 -4
- package/dist/{perplexity-WW5N2LW7.js → perplexity-M7LOMJWA.js} +4 -4
- package/dist/{pinecone-RY5AZPQM.js → pinecone-Y7YCJRZM.js} +4 -4
- package/dist/{pipedrive-OHYQNZTU.js → pipedrive-FLJA62UX.js} +4 -4
- package/dist/{polar-XZIAHXS5.js → polar-AE55Q425.js} +4 -4
- package/dist/{posthog-DTPTWORY.js → posthog-IAY5D3YW.js} +4 -4
- package/dist/{postmark-F4UCQKC3.js → postmark-NMKAXDXZ.js} +4 -4
- package/dist/{prerender-P3Y2MLIC.js → prerender-CDJM7I7N.js} +4 -4
- package/dist/{pubmed-RHU3GXFB.js → pubmed-N437ZJOR.js} +3 -3
- package/dist/{railway-VB6SRLI6.js → railway-NBWYXCUM.js} +3 -3
- package/dist/{readwise-DIMSKJZR.js → readwise-QRO3UC3R.js} +4 -4
- package/dist/{render-XCCDCEEK.js → render-NEW7SROG.js} +4 -4
- package/dist/{replicate-IUEOQ4JC.js → replicate-73YQLRTY.js} +4 -4
- package/dist/{resend-N5MJZH5E.js → resend-R5BAZFHZ.js} +4 -4
- package/dist/{rootly-25T4NBTU.js → rootly-5BLALPCS.js} +4 -4
- package/dist/{scrapfly-PK4XFXUA.js → scrapfly-EHFLNNUZ.js} +4 -4
- package/dist/{scrapingbee-ITPBMWQU.js → scrapingbee-UXLYS72S.js} +4 -4
- package/dist/{screenshot_fyi-5U6ZYPKR.js → screenshot_fyi-KC3NLTJ6.js} +4 -4
- package/dist/{semantic_scholar-BJGCI2MU.js → semantic_scholar-SA5OIUWA.js} +4 -4
- package/dist/{sentry-63IC263P.js → sentry-BAE3XLJN.js} +4 -4
- package/dist/{shodan-2WBIFHFN.js → shodan-FJR6MKKW.js} +4 -4
- package/dist/{shopify-XLLJRAGM.js → shopify-7OA6SLN5.js} +4 -4
- package/dist/{stripe-WLVCY3UN.js → stripe-MUWDZGGF.js} +3 -3
- package/dist/{supabase-BKNZ35ER.js → supabase-SWWGNWYR.js} +4 -4
- package/dist/{tavily-YFSDEUTL.js → tavily-XQNJ5EDI.js} +4 -4
- package/dist/{telegram-FC35W74G.js → telegram-RR6JPZOG.js} +4 -4
- package/dist/{telnyx-MI32XZUG.js → telnyx-7PKXOWSO.js} +4 -4
- package/dist/{todoist-4ZFJKZC6.js → todoist-63X5IUJE.js} +4 -4
- package/dist/{trello-UOGFNMK4.js → trello-MOG63DAI.js} +4 -4
- package/dist/{turso-YTSTW3ZS.js → turso-ENWTSHDL.js} +4 -4
- package/dist/{twilio-XAJDFUQH.js → twilio-FDXUN7VX.js} +3 -3
- package/dist/{umami-ZVIUBPAI.js → umami-TM7PROWF.js} +4 -4
- package/dist/{unsplash-IU3PLDNS.js → unsplash-NWXLKSOW.js} +4 -4
- package/dist/{upstash_redis-3EYJW7SY.js → upstash_redis-CUTQ5L2N.js} +4 -4
- package/dist/{uptimerobot-PJG2ASRU.js → uptimerobot-NNO32M3I.js} +3 -3
- package/dist/{v2ex-KFQWWGFC.js → v2ex-24KMSWS7.js} +4 -4
- package/dist/{vercel-I2ZDOQRL.js → vercel-B45LPF6I.js} +4 -4
- package/dist/{wolfram_alpha_api-BNXOJMB6.js → wolfram_alpha_api-DKJWVR6G.js} +4 -4
- package/dist/{wordpress-36D7BSMP.js → wordpress-XYFSDZXY.js} +4 -4
- package/dist/{workos-6X4JJRRC.js → workos-BEM7EPHM.js} +4 -4
- package/dist/{zhihu-CIXKCHIU.js → zhihu-CFHZ4NC7.js} +4 -4
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createGuardedFetch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3WL6EXYX.js";
|
|
4
4
|
import {
|
|
5
5
|
AnnotationStore,
|
|
6
6
|
CONTEXT_CAPABILITIES,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DEFAULT_STORE_DRIVER_KEY_ROOT,
|
|
10
10
|
DEVICE_CALL_TIMEOUT_MS,
|
|
11
11
|
DeviceGatewaySession,
|
|
12
|
+
DeviceMailboxService,
|
|
12
13
|
FEEDBACK_HIDE_SCORE,
|
|
13
14
|
FeedbackStore,
|
|
14
15
|
HEADER_TB_CONTEXT,
|
|
@@ -40,9 +41,11 @@ import {
|
|
|
40
41
|
TB_ERROR_CODES,
|
|
41
42
|
TOOL_SEARCH_AUDIT_NODE_LIMIT,
|
|
42
43
|
TOOL_SEARCH_BATCH_LIMIT,
|
|
44
|
+
TOOL_SEARCH_DESCRIPTION_BYTES_MAX,
|
|
43
45
|
TOOL_SEARCH_INSERT_SNAPSHOT_SQL,
|
|
44
46
|
TOOL_SEARCH_INSERT_SQL,
|
|
45
47
|
TOOL_SEARCH_PAGE_BYTES,
|
|
48
|
+
TOOL_SEARCH_RANKING_VERSION,
|
|
46
49
|
TOOL_SEARCH_SCHEMA_STATEMENTS,
|
|
47
50
|
TOOL_SEARCH_WORK_LIMIT,
|
|
48
51
|
ZodIssueCode,
|
|
@@ -88,6 +91,16 @@ import {
|
|
|
88
91
|
derivePresence,
|
|
89
92
|
deviceDirectoryHelpModel,
|
|
90
93
|
deviceFsHelpModel,
|
|
94
|
+
deviceOperationClaimRequestSchema,
|
|
95
|
+
deviceOperationClaimResponseSchema,
|
|
96
|
+
deviceOperationCompleteRequestSchema,
|
|
97
|
+
deviceOperationDetailSchema,
|
|
98
|
+
deviceOperationIdentityRequestSchema,
|
|
99
|
+
deviceOperationListRequestSchema,
|
|
100
|
+
deviceOperationListResponseSchema,
|
|
101
|
+
deviceOperationRenewRequestSchema,
|
|
102
|
+
deviceOperationRenewResponseSchema,
|
|
103
|
+
deviceOperationSummarySchema,
|
|
91
104
|
deviceShellHelpModel,
|
|
92
105
|
discriminatedUnion,
|
|
93
106
|
dispatchContextCmd,
|
|
@@ -124,6 +137,7 @@ import {
|
|
|
124
137
|
normalizeEntryPath,
|
|
125
138
|
normalizePath,
|
|
126
139
|
normalizeToolSearchLimit,
|
|
140
|
+
normalizeToolSearchOptions,
|
|
127
141
|
normalizeToolSearchPath,
|
|
128
142
|
normalizeUpstreamError,
|
|
129
143
|
number,
|
|
@@ -146,6 +160,7 @@ import {
|
|
|
146
160
|
pgSearchDialect,
|
|
147
161
|
pluginTokenSecretName,
|
|
148
162
|
prefixUpperBound,
|
|
163
|
+
prepareToolSearchQuery,
|
|
149
164
|
preprocess,
|
|
150
165
|
readinessResponseSchema,
|
|
151
166
|
record,
|
|
@@ -169,8 +184,11 @@ import {
|
|
|
169
184
|
toJSONSchema,
|
|
170
185
|
toolHelpModel,
|
|
171
186
|
toolSearchPageSchema,
|
|
187
|
+
toolSearchRelevanceSchema,
|
|
172
188
|
toolSearchRequestSchema,
|
|
173
189
|
toolSearchSnapshotDigest,
|
|
190
|
+
toolSearchSourceResultSchema,
|
|
191
|
+
toolSearchSourceSchema,
|
|
174
192
|
toolsToHelpModel,
|
|
175
193
|
treeJsonSchema,
|
|
176
194
|
union,
|
|
@@ -178,7 +196,7 @@ import {
|
|
|
178
196
|
url,
|
|
179
197
|
validatePath,
|
|
180
198
|
virtualizeTools
|
|
181
|
-
} from "./chunk-
|
|
199
|
+
} from "./chunk-WIGQCP6O.js";
|
|
182
200
|
|
|
183
201
|
// src/config.ts
|
|
184
202
|
var DEFAULT_PORT = 8787;
|
|
@@ -1545,12 +1563,12 @@ function requireDevice(deps) {
|
|
|
1545
1563
|
}
|
|
1546
1564
|
return deps.device;
|
|
1547
1565
|
}
|
|
1548
|
-
async function
|
|
1566
|
+
async function attemptDevice(deps, deviceId, req) {
|
|
1549
1567
|
const id = crypto.randomUUID();
|
|
1550
1568
|
const upload = req.context === void 0 ? null : await issueDeviceCallUpload(deps, deviceId, id, req.context);
|
|
1551
|
-
let
|
|
1569
|
+
let attempt2;
|
|
1552
1570
|
try {
|
|
1553
|
-
|
|
1571
|
+
attempt2 = await requireDevice(deps).invoke(deviceId, {
|
|
1554
1572
|
id,
|
|
1555
1573
|
path: req.path,
|
|
1556
1574
|
arguments: req.arguments,
|
|
@@ -1560,11 +1578,15 @@ async function invokeDevice(deps, deviceId, req) {
|
|
|
1560
1578
|
await upload?.revoke().catch(() => {
|
|
1561
1579
|
});
|
|
1562
1580
|
}
|
|
1563
|
-
if (!
|
|
1581
|
+
if (attempt2 === null || typeof attempt2 !== "object" || !["completed", "not_dispatched", "unknown"].includes(attempt2.disposition) || attempt2.result === null || typeof attempt2.result !== "object" || !("ok" in attempt2.result) || attempt2.disposition !== "completed" && attempt2.result.ok !== false) {
|
|
1564
1582
|
throw new TBError("unavailable", "device session returned invalid result");
|
|
1565
1583
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1584
|
+
return attempt2;
|
|
1585
|
+
}
|
|
1586
|
+
async function invokeDevice(deps, deviceId, req) {
|
|
1587
|
+
const attempt2 = await attemptDevice(deps, deviceId, req);
|
|
1588
|
+
if (attempt2.result.ok) return attempt2.result.value;
|
|
1589
|
+
throw tbErrorFromBody(attempt2.result.error);
|
|
1568
1590
|
}
|
|
1569
1591
|
function deviceCallContextFrom(ctx) {
|
|
1570
1592
|
const now = Date.now();
|
|
@@ -10023,13 +10045,13 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
10023
10045
|
const buffer = [];
|
|
10024
10046
|
let endipv6Encountered = false;
|
|
10025
10047
|
let endIpv6 = false;
|
|
10026
|
-
let
|
|
10048
|
+
let consume2 = consumeHextets;
|
|
10027
10049
|
for (let i = 0; i < input.length; i++) {
|
|
10028
10050
|
const cursor = input[i];
|
|
10029
10051
|
if (cursor === "[" || cursor === "]") continue;
|
|
10030
10052
|
if (cursor === ":") {
|
|
10031
10053
|
if (endipv6Encountered === true) endIpv6 = true;
|
|
10032
|
-
if (!
|
|
10054
|
+
if (!consume2(buffer, address, output)) break;
|
|
10033
10055
|
if (++tokenCount > 7) {
|
|
10034
10056
|
output.error = true;
|
|
10035
10057
|
break;
|
|
@@ -10038,14 +10060,14 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
10038
10060
|
address.push(":");
|
|
10039
10061
|
continue;
|
|
10040
10062
|
} else if (cursor === "%") {
|
|
10041
|
-
if (!
|
|
10042
|
-
|
|
10063
|
+
if (!consume2(buffer, address, output)) break;
|
|
10064
|
+
consume2 = consumeIsZone;
|
|
10043
10065
|
} else {
|
|
10044
10066
|
buffer.push(cursor);
|
|
10045
10067
|
continue;
|
|
10046
10068
|
}
|
|
10047
10069
|
}
|
|
10048
|
-
if (buffer.length) if (
|
|
10070
|
+
if (buffer.length) if (consume2 === consumeIsZone) output.zone = buffer.join("");
|
|
10049
10071
|
else if (endIpv6) address.push(buffer.join(""));
|
|
10050
10072
|
else address.push(stringArrayToHexStripped(buffer));
|
|
10051
10073
|
output.address = address.join("");
|
|
@@ -19188,12 +19210,51 @@ async function invalidateMcpOAuth(store, nodePath) {
|
|
|
19188
19210
|
await store.delete(KEY_OAUTH_TOKENS + nodePath);
|
|
19189
19211
|
await store.delete(KEY_OAUTH_DISCOVERY + nodePath);
|
|
19190
19212
|
}
|
|
19213
|
+
async function assertMcpOAuthConfig(config, secrets) {
|
|
19214
|
+
if (config === null || typeof config !== "object") return;
|
|
19215
|
+
const candidate = config;
|
|
19216
|
+
if (candidate.kind !== "mcp" || candidate.oauthClient === void 0) return;
|
|
19217
|
+
if (candidate.auth !== "oauth") {
|
|
19218
|
+
throw new TBError("invalid_argument", "mcp oauthClient \u53EA\u80FD\u4E0E auth:'oauth' \u4E00\u8D77\u4F7F\u7528");
|
|
19219
|
+
}
|
|
19220
|
+
if (candidate.oauthClient === null || typeof candidate.oauthClient !== "object") {
|
|
19221
|
+
throw new TBError("invalid_argument", "mcp oauthClient \u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
19222
|
+
}
|
|
19223
|
+
const client = candidate.oauthClient;
|
|
19224
|
+
const unknown2 = Object.keys(client).filter((key) => !["clientId", "clientSecretRef"].includes(key));
|
|
19225
|
+
if (unknown2.length > 0) {
|
|
19226
|
+
throw new TBError(
|
|
19227
|
+
"invalid_argument",
|
|
19228
|
+
`mcp oauthClient \u4E0D\u63A5\u53D7\u5B57\u6BB5:${unknown2.join(", ")};client secret \u5FC5\u987B\u8D70 clientSecretRef`
|
|
19229
|
+
);
|
|
19230
|
+
}
|
|
19231
|
+
if (typeof client.clientId !== "string" || client.clientId.trim() === "") {
|
|
19232
|
+
throw new TBError("invalid_argument", "mcp oauthClient.clientId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
19233
|
+
}
|
|
19234
|
+
if (client.clientSecretRef === void 0) return;
|
|
19235
|
+
if (typeof client.clientSecretRef !== "string" || client.clientSecretRef.trim() === "") {
|
|
19236
|
+
throw new TBError("invalid_argument", "mcp oauthClient.clientSecretRef \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
19237
|
+
}
|
|
19238
|
+
if (await secrets.resolve(client.clientSecretRef) === void 0) {
|
|
19239
|
+
throw new TBError(
|
|
19240
|
+
"invalid_argument",
|
|
19241
|
+
`OAuth client secret ref '${client.clientSecretRef}' \u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u89E3\u5BC6`
|
|
19242
|
+
);
|
|
19243
|
+
}
|
|
19244
|
+
}
|
|
19191
19245
|
function reauthorizeRequired(nodePath) {
|
|
19192
19246
|
return new TBError(
|
|
19193
19247
|
"permission_denied",
|
|
19194
19248
|
`mcp upstream '${nodePath}' requires (re)authorization: run \`tb tool auth ${nodePath}\``
|
|
19195
19249
|
);
|
|
19196
19250
|
}
|
|
19251
|
+
var mcpOAuthFetch = (input, init) => {
|
|
19252
|
+
const headers = new Headers(
|
|
19253
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
19254
|
+
);
|
|
19255
|
+
const tokenRequest = headers.get("content-type")?.toLowerCase().startsWith("application/x-www-form-urlencoded") === true;
|
|
19256
|
+
return fetch(input, tokenRequest ? { ...init, redirect: "error" } : init);
|
|
19257
|
+
};
|
|
19197
19258
|
async function stateCryptoKey(secret) {
|
|
19198
19259
|
const digest = await crypto.subtle.digest(
|
|
19199
19260
|
"SHA-256",
|
|
@@ -19225,7 +19286,7 @@ async function openOAuthState(state, secret) {
|
|
|
19225
19286
|
ciphertext
|
|
19226
19287
|
);
|
|
19227
19288
|
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") {
|
|
19289
|
+
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
19290
|
return null;
|
|
19230
19291
|
}
|
|
19231
19292
|
return payload;
|
|
@@ -19252,11 +19313,31 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19252
19313
|
redirect_uris: [this.redirectUrl],
|
|
19253
19314
|
grant_types: ["authorization_code", "refresh_token"],
|
|
19254
19315
|
response_types: ["code"],
|
|
19255
|
-
|
|
19256
|
-
//
|
|
19316
|
+
// DCR 与 static public client 都是 PKCE public client;confidential client 由 AS
|
|
19317
|
+
// metadata + client_secret 自动协商 basic/post,不在 metadata 里误声明 none。
|
|
19318
|
+
...this.opts.oauthClient?.clientSecretRef === void 0 ? { token_endpoint_auth_method: "none" } : {}
|
|
19257
19319
|
};
|
|
19258
19320
|
}
|
|
19259
|
-
async clientInformation() {
|
|
19321
|
+
async clientInformation(ctx) {
|
|
19322
|
+
const configured = this.opts.oauthClient;
|
|
19323
|
+
if (configured !== void 0) {
|
|
19324
|
+
let clientSecret;
|
|
19325
|
+
if (configured.clientSecretRef !== void 0) {
|
|
19326
|
+
clientSecret = await this.opts.secrets.resolve(configured.clientSecretRef);
|
|
19327
|
+
if (clientSecret === void 0) {
|
|
19328
|
+
throw new TBError(
|
|
19329
|
+
"unavailable",
|
|
19330
|
+
`OAuth client secret ref '${configured.clientSecretRef}' \u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u89E3\u5BC6`,
|
|
19331
|
+
{ retryable: false }
|
|
19332
|
+
);
|
|
19333
|
+
}
|
|
19334
|
+
}
|
|
19335
|
+
return {
|
|
19336
|
+
client_id: configured.clientId,
|
|
19337
|
+
...clientSecret !== void 0 ? { client_secret: clientSecret } : {},
|
|
19338
|
+
...ctx?.issuer !== void 0 ? { issuer: ctx.issuer } : {}
|
|
19339
|
+
};
|
|
19340
|
+
}
|
|
19260
19341
|
const raw = await this.opts.store.get(KEY_OAUTH_CLIENT + this.opts.nodePath);
|
|
19261
19342
|
if (raw === null) {
|
|
19262
19343
|
if (this.opts.mode === "deny") throw reauthorizeRequired(this.opts.nodePath);
|
|
@@ -19265,14 +19346,47 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19265
19346
|
return raw;
|
|
19266
19347
|
}
|
|
19267
19348
|
async saveClientInformation(info) {
|
|
19349
|
+
if (this.opts.oauthClient !== void 0) return;
|
|
19268
19350
|
await this.opts.store.put(KEY_OAUTH_CLIENT + this.opts.nodePath, info);
|
|
19269
19351
|
}
|
|
19270
|
-
async tokens() {
|
|
19352
|
+
async tokens(ctx) {
|
|
19271
19353
|
const raw = await this.opts.store.get(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19272
|
-
|
|
19354
|
+
if (raw === null) return void 0;
|
|
19355
|
+
const tokens = raw;
|
|
19356
|
+
const discovery = await this.opts.store.get(KEY_OAUTH_DISCOVERY + this.opts.nodePath);
|
|
19357
|
+
const discoveredIssuer = discovery?.authorizationServerMetadata?.issuer;
|
|
19358
|
+
if (discoveredIssuer !== void 0 && tokens.issuer !== void 0 && tokens.issuer !== discoveredIssuer) {
|
|
19359
|
+
await this.opts.store.delete(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19360
|
+
return void 0;
|
|
19361
|
+
}
|
|
19362
|
+
let binding = tokens.toolBridgeBinding;
|
|
19363
|
+
const client = await this.clientInformation(ctx);
|
|
19364
|
+
if (binding === void 0 && this.opts.oauthClient === void 0 && client !== void 0 && "redirect_uris" in client && client.redirect_uris?.length === 1) {
|
|
19365
|
+
binding = { clientId: client.client_id, redirectUri: client.redirect_uris[0] };
|
|
19366
|
+
await this.opts.store.put(KEY_OAUTH_TOKENS + this.opts.nodePath, {
|
|
19367
|
+
...tokens,
|
|
19368
|
+
toolBridgeBinding: binding
|
|
19369
|
+
});
|
|
19370
|
+
}
|
|
19371
|
+
const bindingMismatch = binding === void 0 || client === void 0 || binding.clientId !== client.client_id || this.opts.mode === "interactive" && binding.redirectUri !== this.redirectUrl;
|
|
19372
|
+
if (bindingMismatch) {
|
|
19373
|
+
await this.opts.store.delete(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19374
|
+
return void 0;
|
|
19375
|
+
}
|
|
19376
|
+
return tokens;
|
|
19273
19377
|
}
|
|
19274
|
-
async saveTokens(tokens) {
|
|
19275
|
-
await this.
|
|
19378
|
+
async saveTokens(tokens, ctx) {
|
|
19379
|
+
const client = await this.clientInformation(ctx);
|
|
19380
|
+
if (client === void 0) throw new TBError("internal", "OAuth client information missing");
|
|
19381
|
+
const previous = await this.opts.store.get(KEY_OAUTH_TOKENS + this.opts.nodePath);
|
|
19382
|
+
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);
|
|
19383
|
+
if (redirectUri === void 0) {
|
|
19384
|
+
throw new TBError("permission_denied", "OAuth grant redirect binding missing; reauthorize");
|
|
19385
|
+
}
|
|
19386
|
+
await this.opts.store.put(KEY_OAUTH_TOKENS + this.opts.nodePath, {
|
|
19387
|
+
...tokens,
|
|
19388
|
+
toolBridgeBinding: { clientId: client.client_id, redirectUri }
|
|
19389
|
+
});
|
|
19276
19390
|
}
|
|
19277
19391
|
async saveCodeVerifier(codeVerifier) {
|
|
19278
19392
|
this.verifier = codeVerifier;
|
|
@@ -19294,7 +19408,13 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19294
19408
|
p: this.opts.nodePath,
|
|
19295
19409
|
v: this.codeVerifier(),
|
|
19296
19410
|
exp: Math.floor(Date.now() / 1e3) + STATE_TTL_SEC,
|
|
19297
|
-
|
|
19411
|
+
r: this.redirectUrl,
|
|
19412
|
+
...this.opts.oauthClient !== void 0 ? {
|
|
19413
|
+
o: {
|
|
19414
|
+
i: this.opts.oauthClient.clientId,
|
|
19415
|
+
...this.opts.oauthClient.clientSecretRef !== void 0 ? { r: this.opts.oauthClient.clientSecretRef } : {}
|
|
19416
|
+
}
|
|
19417
|
+
} : {}
|
|
19298
19418
|
},
|
|
19299
19419
|
this.opts.encryptionKey
|
|
19300
19420
|
);
|
|
@@ -19311,7 +19431,9 @@ var GatewayMcpOAuthProvider = class {
|
|
|
19311
19431
|
/** SDK 对 InvalidClient/InvalidGrant 等可恢复错误的重试钩子:清对应缓存后重走。 */
|
|
19312
19432
|
async invalidateCredentials(scope) {
|
|
19313
19433
|
const { store, nodePath } = this.opts;
|
|
19314
|
-
if (scope === "all" || scope === "client")
|
|
19434
|
+
if ((scope === "all" || scope === "client") && this.opts.oauthClient === void 0) {
|
|
19435
|
+
await store.delete(KEY_OAUTH_CLIENT + nodePath);
|
|
19436
|
+
}
|
|
19315
19437
|
if (scope === "all" || scope === "tokens") await store.delete(KEY_OAUTH_TOKENS + nodePath);
|
|
19316
19438
|
if (scope === "all" || scope === "discovery") await store.delete(KEY_OAUTH_DISCOVERY + nodePath);
|
|
19317
19439
|
if (scope === "all" || scope === "verifier") this.verifier = void 0;
|
|
@@ -19338,6 +19460,13 @@ async function guardOAuth(fn) {
|
|
|
19338
19460
|
return await fn();
|
|
19339
19461
|
} catch (err) {
|
|
19340
19462
|
if (err instanceof TBError) throw err;
|
|
19463
|
+
if (err instanceof Error && /does not support dynamic client registration/i.test(err.message)) {
|
|
19464
|
+
throw new TBError(
|
|
19465
|
+
"invalid_argument",
|
|
19466
|
+
"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)",
|
|
19467
|
+
{ retryable: false }
|
|
19468
|
+
);
|
|
19469
|
+
}
|
|
19341
19470
|
throw new TBError(
|
|
19342
19471
|
"unavailable",
|
|
19343
19472
|
`OAuth flow failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -19346,11 +19475,13 @@ async function guardOAuth(fn) {
|
|
|
19346
19475
|
}
|
|
19347
19476
|
}
|
|
19348
19477
|
async function startMcpAuthorization(opts) {
|
|
19349
|
-
if (opts.redirectUri !== void 0)
|
|
19350
|
-
|
|
19478
|
+
if (opts.redirectUri !== void 0) assertLocalRedirectUri(opts.redirectUri);
|
|
19479
|
+
const redirectUri = opts.redirectUri ?? `${opts.origin}${OAUTH_CALLBACK_PATH}`;
|
|
19480
|
+
if (opts.oauthClient === void 0) {
|
|
19351
19481
|
const cached = await opts.store.get(KEY_OAUTH_CLIENT + opts.nodePath);
|
|
19352
|
-
if (cached !== null && !(cached.redirect_uris ?? []).includes(
|
|
19482
|
+
if (cached !== null && !(cached.redirect_uris ?? []).includes(redirectUri)) {
|
|
19353
19483
|
await opts.store.delete(KEY_OAUTH_CLIENT + opts.nodePath);
|
|
19484
|
+
await opts.store.delete(KEY_OAUTH_TOKENS + opts.nodePath);
|
|
19354
19485
|
}
|
|
19355
19486
|
}
|
|
19356
19487
|
const provider = new GatewayMcpOAuthProvider({
|
|
@@ -19359,10 +19490,12 @@ async function startMcpAuthorization(opts) {
|
|
|
19359
19490
|
encryptionKey: opts.encryptionKey,
|
|
19360
19491
|
mode: "interactive",
|
|
19361
19492
|
origin: opts.origin,
|
|
19493
|
+
secrets: opts.secrets,
|
|
19494
|
+
...opts.oauthClient !== void 0 ? { oauthClient: opts.oauthClient } : {},
|
|
19362
19495
|
...opts.redirectUri !== void 0 ? { redirectUri: opts.redirectUri } : {}
|
|
19363
19496
|
});
|
|
19364
19497
|
return await guardOAuth(async () => {
|
|
19365
|
-
const result = await auth(provider, { serverUrl: opts.serverUrl });
|
|
19498
|
+
const result = await auth(provider, { serverUrl: opts.serverUrl, fetchFn: mcpOAuthFetch });
|
|
19366
19499
|
if (result === "AUTHORIZED") return { status: "authorized" };
|
|
19367
19500
|
const url2 = provider.capturedAuthorizationUrl;
|
|
19368
19501
|
if (url2 === void 0) {
|
|
@@ -19378,13 +19511,17 @@ async function finishMcpAuthorization(opts) {
|
|
|
19378
19511
|
encryptionKey: opts.encryptionKey,
|
|
19379
19512
|
mode: "interactive",
|
|
19380
19513
|
origin: opts.origin,
|
|
19514
|
+
secrets: opts.secrets,
|
|
19515
|
+
...opts.oauthClient !== void 0 ? { oauthClient: opts.oauthClient } : {},
|
|
19381
19516
|
...opts.redirectUri !== void 0 ? { redirectUri: opts.redirectUri } : {}
|
|
19382
19517
|
});
|
|
19383
19518
|
provider.setCodeVerifier(opts.codeVerifier);
|
|
19384
19519
|
await guardOAuth(async () => {
|
|
19385
19520
|
const result = await auth(provider, {
|
|
19386
19521
|
serverUrl: opts.serverUrl,
|
|
19387
|
-
authorizationCode: opts.code
|
|
19522
|
+
authorizationCode: opts.code,
|
|
19523
|
+
fetchFn: mcpOAuthFetch,
|
|
19524
|
+
...opts.authorizationResponseIssuer !== void 0 ? { iss: opts.authorizationResponseIssuer } : {}
|
|
19388
19525
|
});
|
|
19389
19526
|
if (result !== "AUTHORIZED") {
|
|
19390
19527
|
throw new TBError("internal", "authorization did not complete");
|
|
@@ -19451,7 +19588,7 @@ var noStandaloneSseFetch = (input, init) => {
|
|
|
19451
19588
|
if (String(method).toUpperCase() === "GET" && accept.includes("text/event-stream")) {
|
|
19452
19589
|
return Promise.resolve(new Response(null, { status: 405, statusText: "Method Not Allowed" }));
|
|
19453
19590
|
}
|
|
19454
|
-
return
|
|
19591
|
+
return mcpOAuthFetch(input, init);
|
|
19455
19592
|
};
|
|
19456
19593
|
var ERA_KEY_PREFIX = "mcpera:";
|
|
19457
19594
|
var LEGACY_ERA_HORIZON_SEC = 300;
|
|
@@ -19591,7 +19728,9 @@ function createMcpProvider(config, secrets, opts) {
|
|
|
19591
19728
|
store: opts.oauth.store,
|
|
19592
19729
|
nodePath,
|
|
19593
19730
|
encryptionKey: opts.oauth.encryptionKey,
|
|
19594
|
-
mode: "deny"
|
|
19731
|
+
mode: "deny",
|
|
19732
|
+
secrets,
|
|
19733
|
+
...config.oauthClient !== void 0 ? { oauthClient: config.oauthClient } : {}
|
|
19595
19734
|
})
|
|
19596
19735
|
};
|
|
19597
19736
|
}
|
|
@@ -20509,7 +20648,7 @@ async function processDeviceHello(opts) {
|
|
|
20509
20648
|
input,
|
|
20510
20649
|
authCtx.keyId,
|
|
20511
20650
|
now,
|
|
20512
|
-
input.path === mountPath ? { online: true, lastSeenAt: now } : {}
|
|
20651
|
+
input.path === mountPath ? { deviceId: hello.deviceId, online: true, lastSeenAt: now } : {}
|
|
20513
20652
|
);
|
|
20514
20653
|
} catch (error) {
|
|
20515
20654
|
await searchSync?.abort(marker);
|
|
@@ -20523,6 +20662,64 @@ async function processDeviceHello(opts) {
|
|
|
20523
20662
|
return { mountPath, keyId: authCtx.keyId, searchIndexed };
|
|
20524
20663
|
}
|
|
20525
20664
|
|
|
20665
|
+
// ../app/src/deviceMailbox.ts
|
|
20666
|
+
var KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS = "sys:device-mailbox-cleanup-progress:v1";
|
|
20667
|
+
function positiveInt2(value, fallback, field) {
|
|
20668
|
+
const actual = value ?? fallback;
|
|
20669
|
+
if (!Number.isSafeInteger(actual) || actual < 1) {
|
|
20670
|
+
throw new TBError("invalid_argument", `${field} must be a positive integer`);
|
|
20671
|
+
}
|
|
20672
|
+
return actual;
|
|
20673
|
+
}
|
|
20674
|
+
function parseProgress(value) {
|
|
20675
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || typeof value.cursor !== "string" || !Number.isSafeInteger(value.revision) || value.revision < 1) throw new TBError("internal", "device mailbox cleanup progress is invalid");
|
|
20676
|
+
return value;
|
|
20677
|
+
}
|
|
20678
|
+
function createDeviceMailboxService(deps) {
|
|
20679
|
+
return new DeviceMailboxService(deps.state, deps.encryptionKey);
|
|
20680
|
+
}
|
|
20681
|
+
async function cleanupDeviceMailbox(deps, opts = {}) {
|
|
20682
|
+
await deps.ensureReady?.();
|
|
20683
|
+
if (deps.state.compareAndSwap === void 0) {
|
|
20684
|
+
throw new TBError("unavailable", "device mailbox cleanup requires StateStore.compareAndSwap");
|
|
20685
|
+
}
|
|
20686
|
+
const mailbox = createDeviceMailboxService(deps);
|
|
20687
|
+
const maxPages = Math.min(positiveInt2(opts.maxPages, 8, "maxPages"), 64);
|
|
20688
|
+
const limit = Math.min(positiveInt2(opts.limit, 200, "limit"), 200);
|
|
20689
|
+
const raw = await deps.state.get(KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS);
|
|
20690
|
+
let progress = raw === null ? null : parseProgress(raw);
|
|
20691
|
+
let cursor = progress?.cursor;
|
|
20692
|
+
const aggregate = { deleted: 0, expired: 0, scanned: 0 };
|
|
20693
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber++) {
|
|
20694
|
+
const page = await mailbox.cleanup({ limit, ...cursor === void 0 ? {} : { cursor } });
|
|
20695
|
+
aggregate.deleted += page.deleted;
|
|
20696
|
+
aggregate.expired += page.expired;
|
|
20697
|
+
aggregate.scanned += page.scanned;
|
|
20698
|
+
if (page.cursor === void 0) {
|
|
20699
|
+
if (progress !== null) {
|
|
20700
|
+
await deps.state.compareAndSwap(
|
|
20701
|
+
KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
|
|
20702
|
+
progress.revision,
|
|
20703
|
+
null
|
|
20704
|
+
);
|
|
20705
|
+
}
|
|
20706
|
+
return aggregate;
|
|
20707
|
+
}
|
|
20708
|
+
const next = {
|
|
20709
|
+
cursor: page.cursor,
|
|
20710
|
+
revision: (progress?.revision ?? 0) + 1
|
|
20711
|
+
};
|
|
20712
|
+
if (!await deps.state.compareAndSwap(
|
|
20713
|
+
KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
|
|
20714
|
+
progress?.revision ?? null,
|
|
20715
|
+
next
|
|
20716
|
+
)) return aggregate;
|
|
20717
|
+
progress = next;
|
|
20718
|
+
cursor = next.cursor;
|
|
20719
|
+
}
|
|
20720
|
+
return { ...aggregate, ...cursor === void 0 ? {} : { cursor } };
|
|
20721
|
+
}
|
|
20722
|
+
|
|
20526
20723
|
// ../../node_modules/.pnpm/@modelcontextprotocol+server@2.0.0/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
20527
20724
|
var __create2 = Object.create;
|
|
20528
20725
|
var __defProp2 = Object.defineProperty;
|
|
@@ -28157,13 +28354,13 @@ var require_utils2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
|
|
|
28157
28354
|
const buffer = [];
|
|
28158
28355
|
let endipv6Encountered = false;
|
|
28159
28356
|
let endIpv6 = false;
|
|
28160
|
-
let
|
|
28357
|
+
let consume2 = consumeHextets;
|
|
28161
28358
|
for (let i = 0; i < input.length; i++) {
|
|
28162
28359
|
const cursor = input[i];
|
|
28163
28360
|
if (cursor === "[" || cursor === "]") continue;
|
|
28164
28361
|
if (cursor === ":") {
|
|
28165
28362
|
if (endipv6Encountered === true) endIpv6 = true;
|
|
28166
|
-
if (!
|
|
28363
|
+
if (!consume2(buffer, address, output)) break;
|
|
28167
28364
|
if (++tokenCount > 7) {
|
|
28168
28365
|
output.error = true;
|
|
28169
28366
|
break;
|
|
@@ -28172,14 +28369,14 @@ var require_utils2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
|
|
|
28172
28369
|
address.push(":");
|
|
28173
28370
|
continue;
|
|
28174
28371
|
} else if (cursor === "%") {
|
|
28175
|
-
if (!
|
|
28176
|
-
|
|
28372
|
+
if (!consume2(buffer, address, output)) break;
|
|
28373
|
+
consume2 = consumeIsZone;
|
|
28177
28374
|
} else {
|
|
28178
28375
|
buffer.push(cursor);
|
|
28179
28376
|
continue;
|
|
28180
28377
|
}
|
|
28181
28378
|
}
|
|
28182
|
-
if (buffer.length) if (
|
|
28379
|
+
if (buffer.length) if (consume2 === consumeIsZone) output.zone = buffer.join("");
|
|
28183
28380
|
else if (endIpv6) address.push(buffer.join(""));
|
|
28184
28381
|
else address.push(stringArrayToHexStripped(buffer));
|
|
28185
28382
|
output.address = address.join("");
|
|
@@ -36168,17 +36365,34 @@ function assertSchemaShape(value, path, depth = 0) {
|
|
|
36168
36365
|
list.forEach((sub, i) => assertSchemaShape(sub, `${path}/${key}/${i}`, depth + 1));
|
|
36169
36366
|
}
|
|
36170
36367
|
}
|
|
36171
|
-
function inputSchemaOf(raw) {
|
|
36172
|
-
if (raw === void 0) return { type: "object", properties: {} };
|
|
36173
|
-
|
|
36368
|
+
function inputSchemaOf(raw, delivery) {
|
|
36369
|
+
if (raw === void 0 && delivery === void 0) return { type: "object", properties: {} };
|
|
36370
|
+
const source = raw ?? { type: "object", properties: {} };
|
|
36371
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
36174
36372
|
throw new Error("MCP tool inputSchema must be a JSON Schema object");
|
|
36175
36373
|
}
|
|
36176
|
-
const schema =
|
|
36374
|
+
const schema = source;
|
|
36177
36375
|
if (schema.type !== void 0 && schema.type !== "object") {
|
|
36178
36376
|
throw new Error("MCP tool inputSchema root type must be object");
|
|
36179
36377
|
}
|
|
36180
36378
|
assertSchemaShape(schema, "inputSchema");
|
|
36181
|
-
|
|
36379
|
+
if (delivery !== "mailbox" && delivery !== "both") {
|
|
36380
|
+
return { ...schema, type: "object" };
|
|
36381
|
+
}
|
|
36382
|
+
const properties = schema.properties;
|
|
36383
|
+
if (properties !== void 0 && (properties === null || typeof properties !== "object" || Array.isArray(properties))) throw new Error("MCP tool inputSchema properties must be an object");
|
|
36384
|
+
return {
|
|
36385
|
+
...schema,
|
|
36386
|
+
type: "object",
|
|
36387
|
+
properties: {
|
|
36388
|
+
...properties ?? {},
|
|
36389
|
+
"~delivery": {
|
|
36390
|
+
type: "string",
|
|
36391
|
+
enum: delivery === "both" ? ["realtime", "mailbox", "fallback"] : ["mailbox", "fallback"],
|
|
36392
|
+
description: "Tool Bridge delivery policy; fallback prefers realtime and queues only when dispatch definitely did not occur."
|
|
36393
|
+
}
|
|
36394
|
+
}
|
|
36395
|
+
};
|
|
36182
36396
|
}
|
|
36183
36397
|
function annotationsOf(tool) {
|
|
36184
36398
|
const annotations = {};
|
|
@@ -36204,7 +36418,7 @@ async function projectTools(source, validator) {
|
|
|
36204
36418
|
HTBP ${bridge.invokePath}`;
|
|
36205
36419
|
let inputSchema;
|
|
36206
36420
|
try {
|
|
36207
|
-
inputSchema = inputSchemaOf(bridge.inputSchema);
|
|
36421
|
+
inputSchema = inputSchemaOf(bridge.inputSchema, bridge.delivery);
|
|
36208
36422
|
} catch {
|
|
36209
36423
|
throw new ProtocolError3(
|
|
36210
36424
|
ProtocolErrorCode2.InternalError,
|
|
@@ -36350,6 +36564,121 @@ function createPluginContextProvider(opts) {
|
|
|
36350
36564
|
|
|
36351
36565
|
// ../app/src/providers/remote.ts
|
|
36352
36566
|
var VIA_HEADER = "x-tb-via";
|
|
36567
|
+
function remoteAbortError() {
|
|
36568
|
+
return new TBError("unavailable", "remote request aborted", { retryable: true });
|
|
36569
|
+
}
|
|
36570
|
+
function remoteResponseTooLarge(maxBytes) {
|
|
36571
|
+
return new TBError(
|
|
36572
|
+
"unavailable",
|
|
36573
|
+
`remote response body exceeds ${maxBytes} bytes`,
|
|
36574
|
+
{ retryable: false }
|
|
36575
|
+
);
|
|
36576
|
+
}
|
|
36577
|
+
function isAbortError(error, signal) {
|
|
36578
|
+
return signal?.aborted === true || error instanceof Error && error.name === "AbortError";
|
|
36579
|
+
}
|
|
36580
|
+
function signalAborted(signal) {
|
|
36581
|
+
return signal?.aborted === true;
|
|
36582
|
+
}
|
|
36583
|
+
function declaredContentLength(response) {
|
|
36584
|
+
const value = response.headers.get("content-length");
|
|
36585
|
+
if (value === null || !/^\d+$/.test(value)) return void 0;
|
|
36586
|
+
const parsed = Number(value);
|
|
36587
|
+
return Number.isSafeInteger(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
36588
|
+
}
|
|
36589
|
+
async function readRemoteResponseBody(response, maxBytes, signal) {
|
|
36590
|
+
if (signal?.aborted === true) throw remoteAbortError();
|
|
36591
|
+
const declared = declaredContentLength(response);
|
|
36592
|
+
if (maxBytes !== void 0 && declared !== void 0 && declared > maxBytes) {
|
|
36593
|
+
void response.body?.cancel().catch(() => {
|
|
36594
|
+
});
|
|
36595
|
+
throw remoteResponseTooLarge(maxBytes);
|
|
36596
|
+
}
|
|
36597
|
+
if (response.body === null) return new Uint8Array();
|
|
36598
|
+
const reader = response.body.getReader();
|
|
36599
|
+
const chunks = [];
|
|
36600
|
+
let total = 0;
|
|
36601
|
+
let abortListener;
|
|
36602
|
+
const abortPromise = signal === void 0 ? void 0 : new Promise((_resolve, reject) => {
|
|
36603
|
+
abortListener = () => {
|
|
36604
|
+
reject(remoteAbortError());
|
|
36605
|
+
void reader.cancel().catch(() => {
|
|
36606
|
+
});
|
|
36607
|
+
};
|
|
36608
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
36609
|
+
if (signal.aborted) abortListener();
|
|
36610
|
+
});
|
|
36611
|
+
try {
|
|
36612
|
+
while (true) {
|
|
36613
|
+
const result = await (abortPromise === void 0 ? reader.read() : Promise.race([reader.read(), abortPromise]));
|
|
36614
|
+
if (signalAborted(signal)) throw remoteAbortError();
|
|
36615
|
+
if (result.done) break;
|
|
36616
|
+
total += result.value.byteLength;
|
|
36617
|
+
if (maxBytes !== void 0 && total > maxBytes) {
|
|
36618
|
+
void reader.cancel().catch(() => {
|
|
36619
|
+
});
|
|
36620
|
+
throw remoteResponseTooLarge(maxBytes);
|
|
36621
|
+
}
|
|
36622
|
+
chunks.push(result.value);
|
|
36623
|
+
}
|
|
36624
|
+
} catch (error) {
|
|
36625
|
+
if (error instanceof TBError) throw error;
|
|
36626
|
+
if (isAbortError(error, signal)) throw remoteAbortError();
|
|
36627
|
+
throw normalizeUpstreamError({
|
|
36628
|
+
kind: "network",
|
|
36629
|
+
message: "failed to read remote response body"
|
|
36630
|
+
});
|
|
36631
|
+
} finally {
|
|
36632
|
+
if (abortListener !== void 0) signal?.removeEventListener("abort", abortListener);
|
|
36633
|
+
}
|
|
36634
|
+
const body = new Uint8Array(total);
|
|
36635
|
+
let offset = 0;
|
|
36636
|
+
for (const chunk of chunks) {
|
|
36637
|
+
body.set(chunk, offset);
|
|
36638
|
+
offset += chunk.byteLength;
|
|
36639
|
+
}
|
|
36640
|
+
return body;
|
|
36641
|
+
}
|
|
36642
|
+
var DEFAULT_FEDERATED_SEARCH_SETTINGS = {
|
|
36643
|
+
maxConcurrency: 4,
|
|
36644
|
+
maxResponseBodyBytes: 512 * 1024,
|
|
36645
|
+
maxSources: 16,
|
|
36646
|
+
minChildWorkMs: 200,
|
|
36647
|
+
perHopReturnReserveMs: 100,
|
|
36648
|
+
sessionTtlMs: 5 * 60 * 1e3,
|
|
36649
|
+
totalDeadlineMs: 2500
|
|
36650
|
+
};
|
|
36651
|
+
function federatedSearchSettings(settings) {
|
|
36652
|
+
const configured = settings.federatedSearch ?? {};
|
|
36653
|
+
const positive = (value, fallback) => value !== void 0 && Number.isFinite(value) && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
36654
|
+
return {
|
|
36655
|
+
maxConcurrency: positive(
|
|
36656
|
+
configured.maxConcurrency,
|
|
36657
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.maxConcurrency
|
|
36658
|
+
),
|
|
36659
|
+
maxResponseBodyBytes: positive(
|
|
36660
|
+
configured.maxResponseBodyBytes,
|
|
36661
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.maxResponseBodyBytes
|
|
36662
|
+
),
|
|
36663
|
+
maxSources: positive(configured.maxSources, DEFAULT_FEDERATED_SEARCH_SETTINGS.maxSources),
|
|
36664
|
+
minChildWorkMs: positive(
|
|
36665
|
+
configured.minChildWorkMs,
|
|
36666
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.minChildWorkMs
|
|
36667
|
+
),
|
|
36668
|
+
perHopReturnReserveMs: positive(
|
|
36669
|
+
configured.perHopReturnReserveMs,
|
|
36670
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.perHopReturnReserveMs
|
|
36671
|
+
),
|
|
36672
|
+
sessionTtlMs: positive(
|
|
36673
|
+
configured.sessionTtlMs,
|
|
36674
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.sessionTtlMs
|
|
36675
|
+
),
|
|
36676
|
+
totalDeadlineMs: positive(
|
|
36677
|
+
configured.totalDeadlineMs,
|
|
36678
|
+
DEFAULT_FEDERATED_SEARCH_SETTINGS.totalDeadlineMs
|
|
36679
|
+
)
|
|
36680
|
+
};
|
|
36681
|
+
}
|
|
36353
36682
|
function selfInstanceId(settings, requestUrl) {
|
|
36354
36683
|
if (settings.instanceId !== void 0 && settings.instanceId.length > 0) {
|
|
36355
36684
|
return settings.instanceId;
|
|
@@ -36368,6 +36697,13 @@ function assertRemoteAllowed(baseUrl, settings) {
|
|
|
36368
36697
|
}
|
|
36369
36698
|
}
|
|
36370
36699
|
async function passthroughRemote(opts) {
|
|
36700
|
+
if (opts.maxResponseBodyBytes !== void 0 && (!Number.isSafeInteger(opts.maxResponseBodyBytes) || opts.maxResponseBodyBytes < 0)) {
|
|
36701
|
+
throw new TBError(
|
|
36702
|
+
"invalid_argument",
|
|
36703
|
+
"maxResponseBodyBytes must be a non-negative safe integer"
|
|
36704
|
+
);
|
|
36705
|
+
}
|
|
36706
|
+
if (opts.signal?.aborted === true) throw remoteAbortError();
|
|
36371
36707
|
const secErr = assertSecureUrl(opts.config.baseUrl, opts.settings.allowInsecure);
|
|
36372
36708
|
if (secErr) throw secErr;
|
|
36373
36709
|
if (!checkAllowlist(opts.config.baseUrl, opts.settings.allowlist)) {
|
|
@@ -36392,6 +36728,16 @@ async function passthroughRemote(opts) {
|
|
|
36392
36728
|
if (accept !== null) outHeaders.accept = accept;
|
|
36393
36729
|
const contentType = opts.headers.get("content-type");
|
|
36394
36730
|
if (contentType !== null) outHeaders["content-type"] = contentType;
|
|
36731
|
+
for (const name of [
|
|
36732
|
+
"x-tb-search-remaining-ms",
|
|
36733
|
+
"x-tb-search-session-ttl-ms",
|
|
36734
|
+
"x-tb-search-source-budget",
|
|
36735
|
+
"x-tb-search-validate-snapshot",
|
|
36736
|
+
"x-tb-search-want-snapshot"
|
|
36737
|
+
]) {
|
|
36738
|
+
const value = opts.headers.get(name);
|
|
36739
|
+
if (value !== null) outHeaders[name] = value;
|
|
36740
|
+
}
|
|
36395
36741
|
if (opts.config.skRef !== void 0) {
|
|
36396
36742
|
const cred = await opts.secrets.resolve(opts.config.skRef);
|
|
36397
36743
|
if (cred === void 0) {
|
|
@@ -36419,17 +36765,29 @@ async function passthroughRemote(opts) {
|
|
|
36419
36765
|
resp = await fetch(target, {
|
|
36420
36766
|
method: opts.method,
|
|
36421
36767
|
headers: outHeaders,
|
|
36768
|
+
// remote 请求可能携带高权 service credential 与敏感 arguments。禁止自动跟随任何
|
|
36769
|
+
// 30x,避免 redirect 绕过初始 baseUrl 的 allowlist/HTTPS 校验或把 body 发往另一目标。
|
|
36770
|
+
redirect: "error",
|
|
36771
|
+
...opts.signal === void 0 ? {} : { signal: opts.signal },
|
|
36422
36772
|
...opts.body !== void 0 && opts.body.length > 0 ? { body: opts.body } : {}
|
|
36423
36773
|
});
|
|
36424
36774
|
} catch (err) {
|
|
36775
|
+
if (isAbortError(err, opts.signal)) throw remoteAbortError();
|
|
36425
36776
|
throw normalizeUpstreamError({
|
|
36426
36777
|
kind: "network",
|
|
36427
36778
|
message: err instanceof Error ? err.message : String(err)
|
|
36428
36779
|
});
|
|
36429
36780
|
}
|
|
36430
|
-
const respBody = await
|
|
36781
|
+
const respBody = await readRemoteResponseBody(
|
|
36782
|
+
resp,
|
|
36783
|
+
opts.maxResponseBodyBytes,
|
|
36784
|
+
opts.signal
|
|
36785
|
+
);
|
|
36431
36786
|
const respCt = resp.headers.get("content-type") ?? "application/octet-stream";
|
|
36432
|
-
return new Response(respBody
|
|
36787
|
+
return new Response(respBody.buffer, {
|
|
36788
|
+
status: resp.status,
|
|
36789
|
+
headers: { "content-type": respCt }
|
|
36790
|
+
});
|
|
36433
36791
|
}
|
|
36434
36792
|
|
|
36435
36793
|
// ../app/src/tbApp.ts
|
|
@@ -36438,6 +36796,16 @@ import { Hono } from "hono";
|
|
|
36438
36796
|
// ../core/src/protocol/openapi.ts
|
|
36439
36797
|
var schemaRegistry = external_exports.registry();
|
|
36440
36798
|
var schemas = {
|
|
36799
|
+
DeviceOperationClaimRequest: deviceOperationClaimRequestSchema,
|
|
36800
|
+
DeviceOperationClaimResponse: deviceOperationClaimResponseSchema,
|
|
36801
|
+
DeviceOperationCompleteRequest: deviceOperationCompleteRequestSchema,
|
|
36802
|
+
DeviceOperationDetail: deviceOperationDetailSchema,
|
|
36803
|
+
DeviceOperationIdentityRequest: deviceOperationIdentityRequestSchema,
|
|
36804
|
+
DeviceOperationListRequest: deviceOperationListRequestSchema,
|
|
36805
|
+
DeviceOperationListResponse: deviceOperationListResponseSchema,
|
|
36806
|
+
DeviceOperationRenewRequest: deviceOperationRenewRequestSchema,
|
|
36807
|
+
DeviceOperationRenewResponse: deviceOperationRenewResponseSchema,
|
|
36808
|
+
DeviceOperationSummary: deviceOperationSummarySchema,
|
|
36441
36809
|
Error: tbErrorBodySchema,
|
|
36442
36810
|
FeedbackDetail: feedbackDetailSchema,
|
|
36443
36811
|
FeedbackList: feedbackListSchema,
|
|
@@ -36454,8 +36822,11 @@ var schemas = {
|
|
|
36454
36822
|
OAuthAuthorizeResponse: oauthAuthorizeResponseSchema,
|
|
36455
36823
|
Readiness: readinessResponseSchema,
|
|
36456
36824
|
RegistryNode: registryNodeSchema,
|
|
36825
|
+
SearchRelevance: toolSearchRelevanceSchema,
|
|
36457
36826
|
SearchRequest: toolSearchRequestSchema,
|
|
36458
36827
|
SearchResponse: toolSearchPageSchema,
|
|
36828
|
+
SearchSource: toolSearchSourceSchema,
|
|
36829
|
+
SearchSourceResult: toolSearchSourceResultSchema,
|
|
36459
36830
|
Tree: treeJsonSchema
|
|
36460
36831
|
};
|
|
36461
36832
|
for (const [id, schema] of Object.entries(schemas)) schemaRegistry.add(schema, { id });
|
|
@@ -36670,20 +37041,82 @@ var fixedControlPlaneOpenApi = {
|
|
|
36670
37041
|
"x-tool-bridge-greedy-path": true
|
|
36671
37042
|
}
|
|
36672
37043
|
},
|
|
37044
|
+
"/~device/operations/get": {
|
|
37045
|
+
post: {
|
|
37046
|
+
operationId: "getDeviceOperation",
|
|
37047
|
+
security: bearerSecurity,
|
|
37048
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationIdentityRequest") },
|
|
37049
|
+
responses: { 200: jsonResponse("Device operation detail", "DeviceOperationDetail"), ...errorResponses }
|
|
37050
|
+
}
|
|
37051
|
+
},
|
|
37052
|
+
"/~device/operations/list": {
|
|
37053
|
+
post: {
|
|
37054
|
+
operationId: "listDeviceOperations",
|
|
37055
|
+
security: bearerSecurity,
|
|
37056
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationListRequest") },
|
|
37057
|
+
responses: { 200: jsonResponse("Visible device operation page", "DeviceOperationListResponse"), ...errorResponses }
|
|
37058
|
+
}
|
|
37059
|
+
},
|
|
37060
|
+
"/~device/operations/cancel": {
|
|
37061
|
+
post: {
|
|
37062
|
+
operationId: "cancelDeviceOperation",
|
|
37063
|
+
security: bearerSecurity,
|
|
37064
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationIdentityRequest") },
|
|
37065
|
+
responses: { 200: jsonResponse("Cancelled or cancellation-requested operation", "DeviceOperationDetail"), ...errorResponses }
|
|
37066
|
+
}
|
|
37067
|
+
},
|
|
37068
|
+
"/~device/mailbox/claim": {
|
|
37069
|
+
post: {
|
|
37070
|
+
operationId: "claimDeviceOperation",
|
|
37071
|
+
security: bearerSecurity,
|
|
37072
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationClaimRequest") },
|
|
37073
|
+
responses: { 200: jsonResponse("Claimed operation or next scan cursor", "DeviceOperationClaimResponse"), ...errorResponses }
|
|
37074
|
+
}
|
|
37075
|
+
},
|
|
37076
|
+
"/~device/mailbox/renew": {
|
|
37077
|
+
post: {
|
|
37078
|
+
operationId: "renewDeviceOperation",
|
|
37079
|
+
security: bearerSecurity,
|
|
37080
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationRenewRequest") },
|
|
37081
|
+
responses: { 200: jsonResponse("Renewed operation lease", "DeviceOperationRenewResponse"), ...errorResponses }
|
|
37082
|
+
}
|
|
37083
|
+
},
|
|
37084
|
+
"/~device/mailbox/complete": {
|
|
37085
|
+
post: {
|
|
37086
|
+
operationId: "completeDeviceOperation",
|
|
37087
|
+
security: bearerSecurity,
|
|
37088
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationCompleteRequest") },
|
|
37089
|
+
responses: { 200: jsonResponse("Terminal device operation", "DeviceOperationDetail"), ...errorResponses }
|
|
37090
|
+
}
|
|
37091
|
+
},
|
|
36673
37092
|
"/{commandPath}": {
|
|
36674
37093
|
post: {
|
|
36675
37094
|
"operationId": "invoke",
|
|
36676
37095
|
"security": bearerSecurity,
|
|
36677
|
-
"parameters": [
|
|
36678
|
-
|
|
36679
|
-
|
|
36680
|
-
|
|
36681
|
-
|
|
36682
|
-
|
|
36683
|
-
|
|
37096
|
+
"parameters": [
|
|
37097
|
+
{
|
|
37098
|
+
in: "path",
|
|
37099
|
+
name: "commandPath",
|
|
37100
|
+
required: true,
|
|
37101
|
+
schema: { type: "string" },
|
|
37102
|
+
description: "Complete HTBP command path, including the command/tool leaf."
|
|
37103
|
+
},
|
|
37104
|
+
{
|
|
37105
|
+
in: "query",
|
|
37106
|
+
name: "ttlSeconds",
|
|
37107
|
+
schema: { type: "integer", minimum: 1 },
|
|
37108
|
+
description: "Mailbox operation TTL; used only when ~delivery can enqueue."
|
|
37109
|
+
},
|
|
37110
|
+
{
|
|
37111
|
+
in: "header",
|
|
37112
|
+
name: "x-tb-idempotency-key",
|
|
37113
|
+
schema: { type: "string", minLength: 1, maxLength: 255 },
|
|
37114
|
+
description: "Mailbox retry key; used only when ~delivery can enqueue."
|
|
37115
|
+
}
|
|
37116
|
+
],
|
|
36684
37117
|
"requestBody": {
|
|
36685
37118
|
required: true,
|
|
36686
|
-
description: "Raw command arguments
|
|
37119
|
+
description: "Raw command arguments plus the reserved optional ~delivery control.",
|
|
36687
37120
|
content: { "application/json": { schema: {} } }
|
|
36688
37121
|
},
|
|
36689
37122
|
"responses": {
|
|
@@ -36694,6 +37127,7 @@ var fixedControlPlaneOpenApi = {
|
|
|
36694
37127
|
"text/markdown": { schema: { type: "string" } }
|
|
36695
37128
|
}
|
|
36696
37129
|
},
|
|
37130
|
+
202: jsonResponse("Durably accepted device operation", "DeviceOperationDetail"),
|
|
36697
37131
|
...errorResponses
|
|
36698
37132
|
},
|
|
36699
37133
|
"x-tool-bridge-greedy-path": true
|
|
@@ -36886,6 +37320,239 @@ async function handleFeedbackDelete(c, env) {
|
|
|
36886
37320
|
return feedbackJson(feedbackRemoveResponseSchema, { ok: true });
|
|
36887
37321
|
}
|
|
36888
37322
|
|
|
37323
|
+
// ../app/src/routes/deviceMailbox.ts
|
|
37324
|
+
var DEVICE_MAILBOX_IDEMPOTENCY_HEADER = "x-tb-idempotency-key";
|
|
37325
|
+
var DEVICE_MAILBOX_HTTP_BODY_BYTES = 272 * 1024;
|
|
37326
|
+
var DEVICE_MAILBOX_LIST_LIMIT_DEFAULT = 50;
|
|
37327
|
+
var DEVICE_MAILBOX_LIST_LIMIT_MAX = 200;
|
|
37328
|
+
function invalidRequest(message) {
|
|
37329
|
+
return new TBError("invalid_argument", message);
|
|
37330
|
+
}
|
|
37331
|
+
async function mailboxJsonObject(c, opts = {}) {
|
|
37332
|
+
const declaredLength = Number(c.req.header("content-length"));
|
|
37333
|
+
if (Number.isFinite(declaredLength) && declaredLength > DEVICE_MAILBOX_HTTP_BODY_BYTES) {
|
|
37334
|
+
throw new TBError("rate_limited", "device mailbox request body is too large", {
|
|
37335
|
+
retryable: false
|
|
37336
|
+
});
|
|
37337
|
+
}
|
|
37338
|
+
const reader = c.req.raw.body?.getReader();
|
|
37339
|
+
let raw = "";
|
|
37340
|
+
if (reader !== void 0) {
|
|
37341
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
37342
|
+
let received = 0;
|
|
37343
|
+
try {
|
|
37344
|
+
while (true) {
|
|
37345
|
+
const chunk = await reader.read();
|
|
37346
|
+
if (chunk.done) break;
|
|
37347
|
+
received += chunk.value.byteLength;
|
|
37348
|
+
if (received > DEVICE_MAILBOX_HTTP_BODY_BYTES) {
|
|
37349
|
+
await reader.cancel().catch(() => {
|
|
37350
|
+
});
|
|
37351
|
+
throw new TBError("rate_limited", "device mailbox request body is too large", {
|
|
37352
|
+
retryable: false
|
|
37353
|
+
});
|
|
37354
|
+
}
|
|
37355
|
+
raw += decoder.decode(chunk.value, { stream: true });
|
|
37356
|
+
}
|
|
37357
|
+
raw += decoder.decode();
|
|
37358
|
+
} catch (cause) {
|
|
37359
|
+
if (cause instanceof TBError) throw cause;
|
|
37360
|
+
throw invalidRequest("body must be valid UTF-8 JSON");
|
|
37361
|
+
}
|
|
37362
|
+
}
|
|
37363
|
+
let parsed = opts.allowEmpty === true && raw === "" ? {} : null;
|
|
37364
|
+
if (raw !== "") {
|
|
37365
|
+
try {
|
|
37366
|
+
parsed = JSON.parse(raw);
|
|
37367
|
+
} catch {
|
|
37368
|
+
parsed = null;
|
|
37369
|
+
}
|
|
37370
|
+
}
|
|
37371
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
37372
|
+
throw invalidRequest("body must be a JSON object");
|
|
37373
|
+
}
|
|
37374
|
+
return parsed;
|
|
37375
|
+
}
|
|
37376
|
+
function parseBody(schema, body) {
|
|
37377
|
+
const parsed = schema.safeParse(body);
|
|
37378
|
+
if (!parsed.success) throw invalidRequest(parsed.error.issues[0]?.message ?? "invalid request");
|
|
37379
|
+
return parsed.data;
|
|
37380
|
+
}
|
|
37381
|
+
function deviceOperationTtlSeconds(url2) {
|
|
37382
|
+
for (const key of url2.searchParams.keys()) {
|
|
37383
|
+
if (key !== "ttlSeconds") throw invalidRequest(`unknown enqueue query parameter '${key}'`);
|
|
37384
|
+
}
|
|
37385
|
+
const value = url2.searchParams.get("ttlSeconds");
|
|
37386
|
+
if (value === null) return void 0;
|
|
37387
|
+
if (!/^[1-9]\d*$/.test(value)) throw invalidRequest("ttlSeconds must be a positive integer");
|
|
37388
|
+
const parsed = Number(value);
|
|
37389
|
+
if (!Number.isSafeInteger(parsed)) throw invalidRequest("ttlSeconds must be a safe integer");
|
|
37390
|
+
return parsed;
|
|
37391
|
+
}
|
|
37392
|
+
function canManage(ctx, operation) {
|
|
37393
|
+
return ctx.owner === operation.caller.owner || check(ctx, operation.targetPath, "admin").allow;
|
|
37394
|
+
}
|
|
37395
|
+
function assertCanManage(ctx, operation) {
|
|
37396
|
+
if (!canManage(ctx, operation)) throw TBError.notFound("device operation not found");
|
|
37397
|
+
}
|
|
37398
|
+
async function assertCurrentDeviceCredential(c, target) {
|
|
37399
|
+
const ctx = c.get("ctx");
|
|
37400
|
+
if (ctx.keyId !== target.deviceKeyId) {
|
|
37401
|
+
throw new TBError("permission_denied", "device credential does not own this operation");
|
|
37402
|
+
}
|
|
37403
|
+
let existing = null;
|
|
37404
|
+
try {
|
|
37405
|
+
existing = await new NodeRegistryStore(c.get("store")).get(target.mountPath);
|
|
37406
|
+
} catch {
|
|
37407
|
+
existing = null;
|
|
37408
|
+
}
|
|
37409
|
+
const allowed = checkRegisterPath({
|
|
37410
|
+
action: "write",
|
|
37411
|
+
existing,
|
|
37412
|
+
sk: {
|
|
37413
|
+
id: ctx.keyId,
|
|
37414
|
+
scopes: ctx.scopes,
|
|
37415
|
+
...ctx.registerPaths === void 0 ? {} : { registerPaths: ctx.registerPaths }
|
|
37416
|
+
},
|
|
37417
|
+
targetPath: target.mountPath
|
|
37418
|
+
});
|
|
37419
|
+
if (!allowed.allow) throw allowed.error;
|
|
37420
|
+
}
|
|
37421
|
+
async function enqueueDeviceCommand(c, env, input) {
|
|
37422
|
+
const { arguments: args, command, marker, node } = input;
|
|
37423
|
+
const ctx = c.get("ctx");
|
|
37424
|
+
const result = await env.mailbox().enqueue({
|
|
37425
|
+
arguments: args,
|
|
37426
|
+
caller: { keyId: ctx.keyId, owner: ctx.owner },
|
|
37427
|
+
deviceId: marker.deviceId,
|
|
37428
|
+
deviceKeyId: node.registeredBy,
|
|
37429
|
+
mountPath: marker.mountPath,
|
|
37430
|
+
// 管理面与审计链必须能区分同一 tool 节点下的不同命令;权限仍按前缀 scope 判定。
|
|
37431
|
+
targetPath: `${node.path}/${command}`,
|
|
37432
|
+
path: `${relativeDevicePath(node.path, marker.mountPath)}/${command}`,
|
|
37433
|
+
traceId: ctx.traceId,
|
|
37434
|
+
...c.req.header(DEVICE_MAILBOX_IDEMPOTENCY_HEADER) === void 0 ? {} : { idempotencyKey: c.req.header(DEVICE_MAILBOX_IDEMPOTENCY_HEADER) },
|
|
37435
|
+
...input.ttlSeconds === void 0 ? {} : { ttlSeconds: input.ttlSeconds }
|
|
37436
|
+
});
|
|
37437
|
+
const response = c.json(deviceOperationDetailSchema.parse(result), 202);
|
|
37438
|
+
response.headers.set("x-tb-delivery", "mailbox");
|
|
37439
|
+
return response;
|
|
37440
|
+
}
|
|
37441
|
+
async function handleDeviceMailboxControl(c, env) {
|
|
37442
|
+
const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
|
|
37443
|
+
const body = await mailboxJsonObject(c);
|
|
37444
|
+
const mailbox = env.mailbox();
|
|
37445
|
+
const ctx = c.get("ctx");
|
|
37446
|
+
if (path === "/~device/operations/get") {
|
|
37447
|
+
const input = parseBody(deviceOperationIdentityRequestSchema, body);
|
|
37448
|
+
const result = await mailbox.get(input.deviceId, input.operationId);
|
|
37449
|
+
assertCanManage(ctx, result);
|
|
37450
|
+
return c.json(deviceOperationDetailSchema.parse(result));
|
|
37451
|
+
}
|
|
37452
|
+
if (path === "/~device/operations/list") {
|
|
37453
|
+
const input = parseBody(deviceOperationListRequestSchema, body);
|
|
37454
|
+
const limit = Math.min(
|
|
37455
|
+
input.opts?.limit ?? DEVICE_MAILBOX_LIST_LIMIT_DEFAULT,
|
|
37456
|
+
DEVICE_MAILBOX_LIST_LIMIT_MAX
|
|
37457
|
+
);
|
|
37458
|
+
let cursor = input.opts?.cursor;
|
|
37459
|
+
const items = [];
|
|
37460
|
+
while (items.length < limit) {
|
|
37461
|
+
const previous = cursor;
|
|
37462
|
+
const page = await mailbox.list({
|
|
37463
|
+
deviceId: input.deviceId,
|
|
37464
|
+
limit: limit - items.length,
|
|
37465
|
+
...cursor === void 0 ? {} : { cursor },
|
|
37466
|
+
...input.opts?.states === void 0 ? {} : { states: input.opts.states }
|
|
37467
|
+
});
|
|
37468
|
+
items.push(...page.items.filter((item) => canManage(ctx, item)));
|
|
37469
|
+
cursor = page.cursor;
|
|
37470
|
+
if (cursor === void 0) {
|
|
37471
|
+
return c.json(deviceOperationListResponseSchema.parse({ items }));
|
|
37472
|
+
}
|
|
37473
|
+
if (cursor === previous) {
|
|
37474
|
+
throw new TBError("internal", "device mailbox list cursor did not advance");
|
|
37475
|
+
}
|
|
37476
|
+
}
|
|
37477
|
+
const resumeCursor = cursor;
|
|
37478
|
+
if (resumeCursor === void 0) {
|
|
37479
|
+
return c.json(deviceOperationListResponseSchema.parse({ items }));
|
|
37480
|
+
}
|
|
37481
|
+
let probeCursor = resumeCursor;
|
|
37482
|
+
while (probeCursor !== void 0) {
|
|
37483
|
+
const previous = probeCursor;
|
|
37484
|
+
const page = await mailbox.list({
|
|
37485
|
+
deviceId: input.deviceId,
|
|
37486
|
+
cursor: probeCursor,
|
|
37487
|
+
limit: DEVICE_MAILBOX_LIST_LIMIT_MAX,
|
|
37488
|
+
...input.opts?.states === void 0 ? {} : { states: input.opts.states }
|
|
37489
|
+
});
|
|
37490
|
+
if (page.items.some((item) => canManage(ctx, item))) {
|
|
37491
|
+
return c.json(deviceOperationListResponseSchema.parse({ items, cursor: resumeCursor }));
|
|
37492
|
+
}
|
|
37493
|
+
probeCursor = page.cursor;
|
|
37494
|
+
if (probeCursor === previous) {
|
|
37495
|
+
throw new TBError("internal", "device mailbox list cursor did not advance");
|
|
37496
|
+
}
|
|
37497
|
+
}
|
|
37498
|
+
return c.json(deviceOperationListResponseSchema.parse({ items }));
|
|
37499
|
+
}
|
|
37500
|
+
if (path === "/~device/operations/cancel") {
|
|
37501
|
+
const input = parseBody(deviceOperationIdentityRequestSchema, body);
|
|
37502
|
+
const current = await mailbox.get(input.deviceId, input.operationId);
|
|
37503
|
+
assertCanManage(ctx, current);
|
|
37504
|
+
return c.json(deviceOperationDetailSchema.parse(
|
|
37505
|
+
await mailbox.cancel(input.deviceId, input.operationId)
|
|
37506
|
+
));
|
|
37507
|
+
}
|
|
37508
|
+
throw TBError.notFound("no such path");
|
|
37509
|
+
}
|
|
37510
|
+
async function handleDeviceMailboxData(c, env) {
|
|
37511
|
+
const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
|
|
37512
|
+
const body = await mailboxJsonObject(c);
|
|
37513
|
+
const mailbox = env.mailbox();
|
|
37514
|
+
const ctx = c.get("ctx");
|
|
37515
|
+
const authorize = async (target) => await assertCurrentDeviceCredential(c, target);
|
|
37516
|
+
if (path === "/~device/mailbox/claim") {
|
|
37517
|
+
const input = parseBody(deviceOperationClaimRequestSchema, body);
|
|
37518
|
+
return c.json(deviceOperationClaimResponseSchema.parse(await mailbox.claim({
|
|
37519
|
+
...input,
|
|
37520
|
+
deviceKeyId: ctx.keyId,
|
|
37521
|
+
authorize
|
|
37522
|
+
})));
|
|
37523
|
+
}
|
|
37524
|
+
if (path === "/~device/mailbox/renew") {
|
|
37525
|
+
const input = parseBody(deviceOperationRenewRequestSchema, body);
|
|
37526
|
+
return c.json(deviceOperationRenewResponseSchema.parse(await mailbox.renew({
|
|
37527
|
+
...input,
|
|
37528
|
+
deviceKeyId: ctx.keyId,
|
|
37529
|
+
authorize
|
|
37530
|
+
})));
|
|
37531
|
+
}
|
|
37532
|
+
if (path === "/~device/mailbox/complete") {
|
|
37533
|
+
const input = parseBody(deviceOperationCompleteRequestSchema, body);
|
|
37534
|
+
let completion;
|
|
37535
|
+
if (input.outcome === "succeeded") {
|
|
37536
|
+
completion = { outcome: "succeeded", result: input.result };
|
|
37537
|
+
} else if (input.outcome === "result_unknown") {
|
|
37538
|
+
completion = {
|
|
37539
|
+
outcome: "result_unknown",
|
|
37540
|
+
...input.error === void 0 ? {} : { error: input.error }
|
|
37541
|
+
};
|
|
37542
|
+
} else {
|
|
37543
|
+
completion = { outcome: input.outcome, error: input.error };
|
|
37544
|
+
}
|
|
37545
|
+
return c.json(deviceOperationDetailSchema.parse(await mailbox.complete({
|
|
37546
|
+
deviceId: input.deviceId,
|
|
37547
|
+
operationId: input.operationId,
|
|
37548
|
+
leaseId: input.leaseId,
|
|
37549
|
+
deviceKeyId: ctx.keyId,
|
|
37550
|
+
authorize
|
|
37551
|
+
}, completion)));
|
|
37552
|
+
}
|
|
37553
|
+
throw TBError.notFound("no such path");
|
|
37554
|
+
}
|
|
37555
|
+
|
|
36889
37556
|
// ../app/src/responses.ts
|
|
36890
37557
|
function tbErrorResponse(err) {
|
|
36891
37558
|
return new Response(JSON.stringify(tbErrorBodySchema.parse(err.toJSON())), {
|
|
@@ -36980,6 +37647,7 @@ function renderTreeDsl(tree) {
|
|
|
36980
37647
|
}
|
|
36981
37648
|
|
|
36982
37649
|
// ../app/src/federation.ts
|
|
37650
|
+
var SEARCH_REMAINING_MS_HEADER = "x-tb-search-remaining-ms";
|
|
36983
37651
|
function remoteProtocolError(message) {
|
|
36984
37652
|
return new TBError("unavailable", message, { retryable: false });
|
|
36985
37653
|
}
|
|
@@ -36987,28 +37655,26 @@ function canonicalRemotePath(path, allowRoot) {
|
|
|
36987
37655
|
if (path !== path.replace(/^\/+|\/+$/g, "")) {
|
|
36988
37656
|
throw remoteProtocolError(`remote returned non-canonical path '${path}'`);
|
|
36989
37657
|
}
|
|
37658
|
+
if (path !== path.toLowerCase()) {
|
|
37659
|
+
throw remoteProtocolError(`remote returned non-canonical path '${path}'`);
|
|
37660
|
+
}
|
|
36990
37661
|
const invalid = validatePath(path, { allowRoot });
|
|
36991
37662
|
if (invalid !== null) throw remoteProtocolError(`remote returned invalid path '${path}'`);
|
|
36992
37663
|
for (const segment of path === "" ? [] : path.split("/")) {
|
|
36993
|
-
|
|
36994
|
-
|
|
36995
|
-
|
|
36996
|
-
|
|
36997
|
-
|
|
36998
|
-
|
|
36999
|
-
|
|
37000
|
-
|
|
37001
|
-
|
|
37002
|
-
|
|
37003
|
-
|
|
37004
|
-
|
|
37005
|
-
|
|
37006
|
-
}
|
|
37007
|
-
if (next === decoded) break;
|
|
37008
|
-
if (pass === 3) {
|
|
37009
|
-
throw remoteProtocolError(`remote returned over-encoded path '${path}'`);
|
|
37010
|
-
}
|
|
37011
|
-
decoded = next;
|
|
37664
|
+
if (segment === "." || segment === ".." || segment.startsWith("~") || segment.includes("\\") || segment.includes("?") || segment.includes("#") || [...segment].some((char) => {
|
|
37665
|
+
const code = char.charCodeAt(0);
|
|
37666
|
+
return code <= 31 || code === 127;
|
|
37667
|
+
})) {
|
|
37668
|
+
throw remoteProtocolError(`remote returned unsafe path segment '${segment}'`);
|
|
37669
|
+
}
|
|
37670
|
+
let decoded;
|
|
37671
|
+
try {
|
|
37672
|
+
decoded = decodeURIComponent(segment);
|
|
37673
|
+
} catch {
|
|
37674
|
+
throw remoteProtocolError(`remote returned malformed encoded path '${path}'`);
|
|
37675
|
+
}
|
|
37676
|
+
if (decoded !== segment) {
|
|
37677
|
+
throw remoteProtocolError(`remote returned non-canonical encoded path '${path}'`);
|
|
37012
37678
|
}
|
|
37013
37679
|
}
|
|
37014
37680
|
return path;
|
|
@@ -37016,19 +37682,104 @@ function canonicalRemotePath(path, allowRoot) {
|
|
|
37016
37682
|
function remotePathWithin(nodePath, commandPath) {
|
|
37017
37683
|
return nodePath === "" || commandPath === nodePath || commandPath.startsWith(`${nodePath}/`);
|
|
37018
37684
|
}
|
|
37019
|
-
|
|
37020
|
-
|
|
37021
|
-
|
|
37022
|
-
|
|
37023
|
-
|
|
37685
|
+
var CALLABLE_TREE_KINDS = /* @__PURE__ */ new Set(["device", "http", "mcp", "remote", "tool"]);
|
|
37686
|
+
function visibleRemotePath(ctx, path, kind) {
|
|
37687
|
+
if (!check(ctx, path, "read").allow) return false;
|
|
37688
|
+
return !CALLABLE_TREE_KINDS.has(kind) || check(ctx, path, "call").allow;
|
|
37689
|
+
}
|
|
37690
|
+
var RemotePathProjector = class {
|
|
37691
|
+
constructor(mountPath) {
|
|
37692
|
+
this.mountPath = mountPath;
|
|
37693
|
+
const invalid = validatePath(mountPath);
|
|
37694
|
+
if (invalid !== null) throw remoteProtocolError(`invalid remote mount path '${mountPath}'`);
|
|
37695
|
+
}
|
|
37696
|
+
mountPath;
|
|
37697
|
+
childPath(localPath) {
|
|
37698
|
+
if (localPath === this.mountPath) return "";
|
|
37699
|
+
if (!localPath.startsWith(`${this.mountPath}/`)) {
|
|
37700
|
+
throw remoteProtocolError(`local path '${localPath}' escapes remote mount '${this.mountPath}'`);
|
|
37701
|
+
}
|
|
37702
|
+
return localPath.slice(this.mountPath.length + 1);
|
|
37703
|
+
}
|
|
37704
|
+
localPath(childPath, allowRoot = true) {
|
|
37705
|
+
const canonical = canonicalRemotePath(childPath, allowRoot);
|
|
37706
|
+
return canonical === "" ? this.mountPath : `${this.mountPath}/${canonical}`;
|
|
37707
|
+
}
|
|
37708
|
+
childCommandPath(path) {
|
|
37709
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
37710
|
+
throw remoteProtocolError(`remote ~help returned invalid command path '${path}'`);
|
|
37711
|
+
}
|
|
37712
|
+
return canonicalRemotePath(path.slice(1), false);
|
|
37713
|
+
}
|
|
37714
|
+
projectHelp(help, requestedLocalPath, ctx) {
|
|
37715
|
+
const expectedChildPath = this.childPath(requestedLocalPath);
|
|
37716
|
+
const nodeChildPath = canonicalRemotePath(help.node.path, expectedChildPath === "");
|
|
37717
|
+
if (nodeChildPath !== expectedChildPath) {
|
|
37718
|
+
throw remoteProtocolError(
|
|
37719
|
+
`remote ~help path '${nodeChildPath}' does not match request '${expectedChildPath}'`
|
|
37720
|
+
);
|
|
37721
|
+
}
|
|
37722
|
+
const nodePath = this.localPath(nodeChildPath, true);
|
|
37723
|
+
const cmds = help.cmds.map((command) => {
|
|
37724
|
+
const childCommandPath = this.childCommandPath(command.path);
|
|
37725
|
+
if (!remotePathWithin(nodeChildPath, childCommandPath)) {
|
|
37726
|
+
throw remoteProtocolError(`remote ~help command '${command.path}' escapes its node`);
|
|
37727
|
+
}
|
|
37728
|
+
return { ...command, path: `/${this.localPath(childCommandPath, false)}` };
|
|
37729
|
+
});
|
|
37730
|
+
const children = help.children?.map((child) => {
|
|
37731
|
+
const childPath = canonicalRemotePath(child.path, false);
|
|
37732
|
+
const parent = childPath.split("/").slice(0, -1).join("/");
|
|
37733
|
+
if (parent !== nodeChildPath) {
|
|
37734
|
+
throw remoteProtocolError(`remote ~help child '${childPath}' is not a direct descendant`);
|
|
37735
|
+
}
|
|
37736
|
+
return { ...child, path: this.localPath(childPath, false) };
|
|
37737
|
+
}).filter((child) => ctx === void 0 || visibleRemotePath(ctx, child.path, child.kind));
|
|
37738
|
+
return {
|
|
37739
|
+
node: { ...help.node, path: nodePath },
|
|
37740
|
+
cmds,
|
|
37741
|
+
...children === void 0 ? {} : { children },
|
|
37742
|
+
...help.feedback === void 0 ? {} : { feedback: help.feedback },
|
|
37743
|
+
...help.hint === void 0 ? {} : { hint: help.hint },
|
|
37744
|
+
...help.note === void 0 ? {} : { note: help.note }
|
|
37745
|
+
};
|
|
37024
37746
|
}
|
|
37025
|
-
|
|
37026
|
-
|
|
37027
|
-
|
|
37028
|
-
|
|
37029
|
-
|
|
37030
|
-
|
|
37031
|
-
|
|
37747
|
+
projectTree(tree, requestedLocalPath, ctx) {
|
|
37748
|
+
const expectedChildPath = this.childPath(requestedLocalPath);
|
|
37749
|
+
const rootChildPath = canonicalRemotePath(tree.path, expectedChildPath === "");
|
|
37750
|
+
if (rootChildPath !== expectedChildPath) {
|
|
37751
|
+
throw remoteProtocolError(
|
|
37752
|
+
`remote ~tree path '${rootChildPath}' does not match request '${expectedChildPath}'`
|
|
37753
|
+
);
|
|
37754
|
+
}
|
|
37755
|
+
const walk = (node, childPath) => {
|
|
37756
|
+
const localPath = this.localPath(childPath, childPath === "");
|
|
37757
|
+
const children = (node.children ?? []).map((child) => {
|
|
37758
|
+
const canonical = canonicalRemotePath(child.path, false);
|
|
37759
|
+
const parent = canonical.split("/").slice(0, -1).join("/");
|
|
37760
|
+
if (parent !== childPath) {
|
|
37761
|
+
throw remoteProtocolError(`remote ~tree child '${canonical}' is not a direct descendant`);
|
|
37762
|
+
}
|
|
37763
|
+
return walk(child, canonical);
|
|
37764
|
+
}).filter((child) => visibleRemotePath(ctx, child.path, child.kind));
|
|
37765
|
+
return {
|
|
37766
|
+
path: localPath,
|
|
37767
|
+
kind: node.kind,
|
|
37768
|
+
description: node.description,
|
|
37769
|
+
...children.length === 0 ? {} : { children },
|
|
37770
|
+
...node.presence === void 0 ? {} : { presence: node.presence },
|
|
37771
|
+
...node.truncated === void 0 ? {} : { truncated: node.truncated }
|
|
37772
|
+
};
|
|
37773
|
+
};
|
|
37774
|
+
return walk(tree, rootChildPath);
|
|
37775
|
+
}
|
|
37776
|
+
};
|
|
37777
|
+
async function remotePathProjectorIfMatch(registry, treePath) {
|
|
37778
|
+
const resolved = await registry.resolve(treePath).catch(() => null);
|
|
37779
|
+
if (resolved === null || resolved.node.kind !== "remote" || resolved.node.config?.kind !== "remote") {
|
|
37780
|
+
return null;
|
|
37781
|
+
}
|
|
37782
|
+
return new RemotePathProjector(resolved.node.path);
|
|
37032
37783
|
}
|
|
37033
37784
|
async function resolveRemoteSettings(state, base) {
|
|
37034
37785
|
const runtime = await new RemoteAllowlistStore(state).hosts();
|
|
@@ -37051,6 +37802,17 @@ async function remotePassthroughIfMatch(c, ctx, registry, treePath, reservedTail
|
|
|
37051
37802
|
}
|
|
37052
37803
|
const requestPath = reservedTail === null ? treePath : `${treePath}/${reservedTail}`;
|
|
37053
37804
|
const body = method === "POST" ? await c.req.text() : void 0;
|
|
37805
|
+
const outboundHeaders = new Headers(headers);
|
|
37806
|
+
const searchSettings = federatedSearchSettings(deps.remote);
|
|
37807
|
+
const rawRemaining = outboundHeaders.get(SEARCH_REMAINING_MS_HEADER);
|
|
37808
|
+
const requestedRemaining = rawRemaining !== null && /^\d+$/u.test(rawRemaining) ? Number(rawRemaining) : void 0;
|
|
37809
|
+
const remainingMs = requestedRemaining !== void 0 && Number.isSafeInteger(requestedRemaining) && requestedRemaining > 0 ? Math.min(requestedRemaining, searchSettings.totalDeadlineMs) : void 0;
|
|
37810
|
+
if (remainingMs !== void 0) {
|
|
37811
|
+
outboundHeaders.set(
|
|
37812
|
+
SEARCH_REMAINING_MS_HEADER,
|
|
37813
|
+
String(Math.max(1, remainingMs - searchSettings.perHopReturnReserveMs))
|
|
37814
|
+
);
|
|
37815
|
+
}
|
|
37054
37816
|
return await passthroughRemote({
|
|
37055
37817
|
actor: { keyId: ctx.keyId, owner: ctx.owner, traceId: ctx.traceId },
|
|
37056
37818
|
config: node.config,
|
|
@@ -37058,7 +37820,11 @@ async function remotePassthroughIfMatch(c, ctx, registry, treePath, reservedTail
|
|
|
37058
37820
|
requestPath,
|
|
37059
37821
|
method,
|
|
37060
37822
|
...body !== void 0 ? { body } : {},
|
|
37061
|
-
headers,
|
|
37823
|
+
headers: outboundHeaders,
|
|
37824
|
+
...remainingMs === void 0 ? {} : {
|
|
37825
|
+
maxResponseBodyBytes: searchSettings.maxResponseBodyBytes,
|
|
37826
|
+
signal: AbortSignal.timeout(remainingMs)
|
|
37827
|
+
},
|
|
37062
37828
|
secrets: deps.secrets,
|
|
37063
37829
|
settings: await resolveRemoteSettings(deps.state, deps.remote),
|
|
37064
37830
|
requestUrl: c.req.url
|
|
@@ -37082,15 +37848,14 @@ async function remoteTreeChildren(c, ctx, registry, treePath, deps) {
|
|
|
37082
37848
|
retryable: resp.status >= 500
|
|
37083
37849
|
});
|
|
37084
37850
|
}
|
|
37085
|
-
const
|
|
37086
|
-
|
|
37851
|
+
const rawRemoteTree = await resp.json().catch(() => null);
|
|
37852
|
+
const parsedRemoteTree = treeJsonSchema.safeParse(rawRemoteTree);
|
|
37853
|
+
if (!parsedRemoteTree.success) {
|
|
37087
37854
|
throw new TBError("unavailable", "remote ~tree returned invalid JSON", { retryable: false });
|
|
37088
37855
|
}
|
|
37089
|
-
const
|
|
37090
|
-
|
|
37091
|
-
|
|
37092
|
-
}
|
|
37093
|
-
return (remoteTree.children ?? []).map((child) => localizeRemoteEntry(resolved.node.path, remotePath, child));
|
|
37856
|
+
const projector = new RemotePathProjector(resolved.node.path);
|
|
37857
|
+
const projected = projector.projectTree(parsedRemoteTree.data, treePath, ctx);
|
|
37858
|
+
return projected.children ?? [];
|
|
37094
37859
|
}
|
|
37095
37860
|
function assertRemoteConfigAllowed(config, settings) {
|
|
37096
37861
|
if (config === null || typeof config !== "object") return;
|
|
@@ -37164,6 +37929,7 @@ async function handleRegister(c, env) {
|
|
|
37164
37929
|
}
|
|
37165
37930
|
await assertRegisterPath2(registry, ctx, body.path, "write", deps);
|
|
37166
37931
|
assertSecretRefUse(ctx.scopes, body.config);
|
|
37932
|
+
await assertMcpOAuthConfig(body.config, deps.secrets);
|
|
37167
37933
|
await assertContextConfig(body.config, deps);
|
|
37168
37934
|
await assertSkillhubConfig(body.config, deps);
|
|
37169
37935
|
await assertToolConfig(body.config, deps, ctx, body.path);
|
|
@@ -37230,7 +37996,9 @@ async function handleAuthorize(c, env) {
|
|
|
37230
37996
|
encryptionKey: encKey,
|
|
37231
37997
|
nodePath: path,
|
|
37232
37998
|
serverUrl: node.config.url,
|
|
37999
|
+
secrets: deps.secrets,
|
|
37233
38000
|
origin: deps.canonicalOrigin ?? new URL(c.req.url).origin,
|
|
38001
|
+
...node.config.oauthClient !== void 0 ? { oauthClient: node.config.oauthClient } : {},
|
|
37234
38002
|
...redirectUri !== void 0 ? { redirectUri } : {}
|
|
37235
38003
|
});
|
|
37236
38004
|
return new Response(JSON.stringify(oauthAuthorizeResponseSchema.parse(result)), {
|
|
@@ -37574,15 +38342,25 @@ function registerPublicRoutes(app, env) {
|
|
|
37574
38342
|
if (node.kind !== "mcp" || node.config?.kind !== "mcp" || node.config.auth !== "oauth") {
|
|
37575
38343
|
return renderOAuthCallbackHtml(false, "target node is not an OAuth-backed mount");
|
|
37576
38344
|
}
|
|
38345
|
+
const currentClient = node.config.oauthClient;
|
|
38346
|
+
if (payload.o === void 0 !== (currentClient === void 0) || payload.o !== void 0 && (currentClient?.clientId !== payload.o.i || currentClient.clientSecretRef !== payload.o.r)) {
|
|
38347
|
+
return renderOAuthCallbackHtml(
|
|
38348
|
+
false,
|
|
38349
|
+
"OAuth client configuration changed; restart authorization"
|
|
38350
|
+
);
|
|
38351
|
+
}
|
|
37577
38352
|
try {
|
|
37578
38353
|
await finishMcpAuthorization({
|
|
37579
38354
|
store: deps.state,
|
|
37580
38355
|
encryptionKey: encKey,
|
|
37581
38356
|
nodePath: payload.p,
|
|
37582
38357
|
serverUrl: node.config.url,
|
|
38358
|
+
secrets: deps.secrets,
|
|
37583
38359
|
origin: deps.canonicalOrigin ?? new URL(c.req.url).origin,
|
|
37584
38360
|
code,
|
|
37585
38361
|
codeVerifier: payload.v,
|
|
38362
|
+
...q.iss !== void 0 ? { authorizationResponseIssuer: q.iss } : {},
|
|
38363
|
+
...node.config.oauthClient !== void 0 ? { oauthClient: node.config.oauthClient } : {},
|
|
37586
38364
|
// 本地回调通道(CLI --local):兑换必须复用授权时的 redirect_uri。
|
|
37587
38365
|
...payload.r !== void 0 ? { redirectUri: payload.r } : {}
|
|
37588
38366
|
});
|
|
@@ -37595,7 +38373,1505 @@ function registerPublicRoutes(app, env) {
|
|
|
37595
38373
|
);
|
|
37596
38374
|
}
|
|
37597
38375
|
|
|
38376
|
+
// ../app/src/search/federatedSession.ts
|
|
38377
|
+
var FEDERATED_SEARCH_HANDLE_PREFIX = "fsc1_";
|
|
38378
|
+
var FEDERATED_SEARCH_HANDLE_RANDOM_BYTES = 24;
|
|
38379
|
+
var FEDERATED_SEARCH_HANDLE_LENGTH = 37;
|
|
38380
|
+
var FEDERATED_SEARCH_SESSION_KEY_PREFIX = "fsearch:session:v1:";
|
|
38381
|
+
var FEDERATED_SEARCH_QUOTA_KEY = "fsearch:quota:v1";
|
|
38382
|
+
var HANDLE_PATTERN = /^fsc1_[A-Za-z0-9_-]{32}$/;
|
|
38383
|
+
var MAX_SAFE_JSON_DEPTH = 64;
|
|
38384
|
+
var MAX_SAFE_JSON_NODES = 1e4;
|
|
38385
|
+
var DEFAULT_MAX_RECORD_BYTES = 1024 * 1024;
|
|
38386
|
+
var DEFAULT_MAX_BYTES_GLOBAL = 128 * 1024 * 1024;
|
|
38387
|
+
var DEFAULT_MAX_BYTES_PER_ACTOR = 32 * 1024 * 1024;
|
|
38388
|
+
var DEFAULT_MAX_BYTES_PER_SESSION = 8 * 1024 * 1024;
|
|
38389
|
+
var DEFAULT_MAX_GENERATIONS_PER_SESSION = 1024;
|
|
38390
|
+
var DEFAULT_MAX_SESSIONS_GLOBAL = 256;
|
|
38391
|
+
var DEFAULT_MAX_SESSIONS_PER_ACTOR = 64;
|
|
38392
|
+
var QUOTA_CAS_ATTEMPTS = 32;
|
|
38393
|
+
var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set([
|
|
38394
|
+
"$schema",
|
|
38395
|
+
"authorization",
|
|
38396
|
+
"baseurl",
|
|
38397
|
+
"credential",
|
|
38398
|
+
"inputschema",
|
|
38399
|
+
"outputschema",
|
|
38400
|
+
"schema",
|
|
38401
|
+
"secret",
|
|
38402
|
+
"sk",
|
|
38403
|
+
"token"
|
|
38404
|
+
]);
|
|
38405
|
+
function sessionUnavailable(message) {
|
|
38406
|
+
return new TBError("unavailable", message, { retryable: false });
|
|
38407
|
+
}
|
|
38408
|
+
function invalidSession() {
|
|
38409
|
+
return new TBError("invalid_argument", "federated search session is invalid or expired", {
|
|
38410
|
+
retryable: false
|
|
38411
|
+
});
|
|
38412
|
+
}
|
|
38413
|
+
function corruptSession() {
|
|
38414
|
+
return new TBError("internal", "federated search session state is corrupt", {
|
|
38415
|
+
retryable: false
|
|
38416
|
+
});
|
|
38417
|
+
}
|
|
38418
|
+
function isPlainObject3(value) {
|
|
38419
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
38420
|
+
const prototype = Object.getPrototypeOf(value);
|
|
38421
|
+
return prototype === Object.prototype || prototype === null;
|
|
38422
|
+
}
|
|
38423
|
+
function hasExactKeys(value, expected) {
|
|
38424
|
+
const actual = Object.keys(value).sort();
|
|
38425
|
+
const sortedExpected = [...expected].sort();
|
|
38426
|
+
return actual.length === sortedExpected.length && actual.every((key, index) => key === sortedExpected[index]);
|
|
38427
|
+
}
|
|
38428
|
+
function validOpaque(value) {
|
|
38429
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value);
|
|
38430
|
+
}
|
|
38431
|
+
function validExpiry(value) {
|
|
38432
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
38433
|
+
}
|
|
38434
|
+
function normalizedStateKey(key) {
|
|
38435
|
+
return key.replaceAll("-", "").replaceAll("_", "").toLowerCase();
|
|
38436
|
+
}
|
|
38437
|
+
function safeStateValue(value, state, depth = 0) {
|
|
38438
|
+
state.nodes++;
|
|
38439
|
+
if (state.nodes > MAX_SAFE_JSON_NODES || depth > MAX_SAFE_JSON_DEPTH) {
|
|
38440
|
+
throw new Error("federated search session value exceeds structural limits");
|
|
38441
|
+
}
|
|
38442
|
+
if (value === null || typeof value === "boolean") return value;
|
|
38443
|
+
if (typeof value === "number") {
|
|
38444
|
+
if (!Number.isFinite(value)) throw new Error("non-finite number");
|
|
38445
|
+
return value;
|
|
38446
|
+
}
|
|
38447
|
+
if (typeof value === "string") return value;
|
|
38448
|
+
if (Array.isArray(value)) {
|
|
38449
|
+
return Object.freeze(value.map((item) => safeStateValue(item, state, depth + 1)));
|
|
38450
|
+
}
|
|
38451
|
+
if (!isPlainObject3(value)) throw new Error("non-JSON value");
|
|
38452
|
+
const clone = {};
|
|
38453
|
+
for (const [key, item] of Object.entries(value)) {
|
|
38454
|
+
if (FORBIDDEN_STATE_KEYS.has(normalizedStateKey(key))) {
|
|
38455
|
+
throw new Error(`forbidden state field '${key}'`);
|
|
38456
|
+
}
|
|
38457
|
+
clone[key] = safeStateValue(item, state, depth + 1);
|
|
38458
|
+
}
|
|
38459
|
+
return Object.freeze(clone);
|
|
38460
|
+
}
|
|
38461
|
+
function parseRecord(value) {
|
|
38462
|
+
if (!isPlainObject3(value) || !hasExactKeys(value, [
|
|
38463
|
+
"actorKeyId",
|
|
38464
|
+
"excludedStatuses",
|
|
38465
|
+
"expiresAt",
|
|
38466
|
+
"federationPolicy",
|
|
38467
|
+
"generation",
|
|
38468
|
+
"nextHandle",
|
|
38469
|
+
"page",
|
|
38470
|
+
"rankingVersion",
|
|
38471
|
+
"requestDigest",
|
|
38472
|
+
"revision",
|
|
38473
|
+
"sessionId",
|
|
38474
|
+
"sourceContinuations",
|
|
38475
|
+
"topologyDigest"
|
|
38476
|
+
])) {
|
|
38477
|
+
throw corruptSession();
|
|
38478
|
+
}
|
|
38479
|
+
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))) {
|
|
38480
|
+
throw corruptSession();
|
|
38481
|
+
}
|
|
38482
|
+
try {
|
|
38483
|
+
const state = { nodes: 0 };
|
|
38484
|
+
return Object.freeze({
|
|
38485
|
+
actorKeyId: value.actorKeyId,
|
|
38486
|
+
excludedStatuses: safeStateValue(value.excludedStatuses, state),
|
|
38487
|
+
expiresAt: value.expiresAt,
|
|
38488
|
+
federationPolicy: safeStateValue(value.federationPolicy, state),
|
|
38489
|
+
generation: value.generation,
|
|
38490
|
+
nextHandle: value.nextHandle,
|
|
38491
|
+
page: safeStateValue(value.page, state),
|
|
38492
|
+
rankingVersion: value.rankingVersion,
|
|
38493
|
+
requestDigest: value.requestDigest,
|
|
38494
|
+
revision: 0,
|
|
38495
|
+
sessionId: value.sessionId,
|
|
38496
|
+
sourceContinuations: safeStateValue(value.sourceContinuations, state),
|
|
38497
|
+
topologyDigest: value.topologyDigest
|
|
38498
|
+
});
|
|
38499
|
+
} catch (error) {
|
|
38500
|
+
if (error instanceof TBError) throw error;
|
|
38501
|
+
throw corruptSession();
|
|
38502
|
+
}
|
|
38503
|
+
}
|
|
38504
|
+
function parseCreate(handle, input) {
|
|
38505
|
+
try {
|
|
38506
|
+
return parseRecord({
|
|
38507
|
+
...input,
|
|
38508
|
+
revision: 0,
|
|
38509
|
+
sessionId: input.sessionId ?? handle
|
|
38510
|
+
});
|
|
38511
|
+
} catch {
|
|
38512
|
+
throw new TBError("invalid_argument", "federated search session input is invalid", {
|
|
38513
|
+
retryable: false
|
|
38514
|
+
});
|
|
38515
|
+
}
|
|
38516
|
+
}
|
|
38517
|
+
function parseQuotaRecord(value) {
|
|
38518
|
+
if (!isPlainObject3(value) || !hasExactKeys(value, ["revision", "sessions"])) {
|
|
38519
|
+
throw corruptSession();
|
|
38520
|
+
}
|
|
38521
|
+
if (!Number.isSafeInteger(value.revision) || value.revision < 0 || !Array.isArray(value.sessions)) {
|
|
38522
|
+
throw corruptSession();
|
|
38523
|
+
}
|
|
38524
|
+
const sessions = value.sessions.map((item) => {
|
|
38525
|
+
if (!isPlainObject3(item) || !hasExactKeys(item, [
|
|
38526
|
+
"actorKeyId",
|
|
38527
|
+
"bytes",
|
|
38528
|
+
"expiresAt",
|
|
38529
|
+
"generation",
|
|
38530
|
+
"generationBytes",
|
|
38531
|
+
"generationId",
|
|
38532
|
+
"id"
|
|
38533
|
+
]) || !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)) {
|
|
38534
|
+
throw corruptSession();
|
|
38535
|
+
}
|
|
38536
|
+
return {
|
|
38537
|
+
actorKeyId: item.actorKeyId,
|
|
38538
|
+
bytes: item.bytes,
|
|
38539
|
+
expiresAt: item.expiresAt,
|
|
38540
|
+
generation: item.generation,
|
|
38541
|
+
generationBytes: item.generationBytes,
|
|
38542
|
+
generationId: item.generationId,
|
|
38543
|
+
id: item.id
|
|
38544
|
+
};
|
|
38545
|
+
});
|
|
38546
|
+
return { revision: value.revision, sessions };
|
|
38547
|
+
}
|
|
38548
|
+
function isFederatedSearchSessionHandle(value) {
|
|
38549
|
+
return typeof value === "string" && value.length === FEDERATED_SEARCH_HANDLE_LENGTH && HANDLE_PATTERN.test(value);
|
|
38550
|
+
}
|
|
38551
|
+
async function federatedSearchSessionStateKey(handle) {
|
|
38552
|
+
if (!isFederatedSearchSessionHandle(handle)) throw invalidSession();
|
|
38553
|
+
return FEDERATED_SEARCH_SESSION_KEY_PREFIX + await sha256Hex(handle);
|
|
38554
|
+
}
|
|
38555
|
+
var FederatedSearchSessionStore = class {
|
|
38556
|
+
constructor(state, opts = {}) {
|
|
38557
|
+
this.state = state;
|
|
38558
|
+
if (state.compareAndSwap === void 0) {
|
|
38559
|
+
throw sessionUnavailable("federated search sessions require StateStore.compareAndSwap");
|
|
38560
|
+
}
|
|
38561
|
+
this.cas = state.compareAndSwap.bind(state);
|
|
38562
|
+
this.maxBytesGlobal = opts.maxBytesGlobal ?? DEFAULT_MAX_BYTES_GLOBAL;
|
|
38563
|
+
this.maxBytesPerActor = opts.maxBytesPerActor ?? DEFAULT_MAX_BYTES_PER_ACTOR;
|
|
38564
|
+
this.maxBytesPerSession = opts.maxBytesPerSession ?? DEFAULT_MAX_BYTES_PER_SESSION;
|
|
38565
|
+
this.maxGenerationsPerSession = opts.maxGenerationsPerSession ?? DEFAULT_MAX_GENERATIONS_PER_SESSION;
|
|
38566
|
+
this.maxRecordBytes = opts.maxRecordBytes ?? DEFAULT_MAX_RECORD_BYTES;
|
|
38567
|
+
this.maxSessionsGlobal = opts.maxSessionsGlobal ?? DEFAULT_MAX_SESSIONS_GLOBAL;
|
|
38568
|
+
this.maxSessionsPerActor = opts.maxSessionsPerActor ?? DEFAULT_MAX_SESSIONS_PER_ACTOR;
|
|
38569
|
+
this.now = opts.now ?? (() => Date.now());
|
|
38570
|
+
this.randomBytes = opts.randomBytes ?? ((length) => crypto.getRandomValues(new Uint8Array(length)));
|
|
38571
|
+
}
|
|
38572
|
+
state;
|
|
38573
|
+
cas;
|
|
38574
|
+
now;
|
|
38575
|
+
randomBytes;
|
|
38576
|
+
maxBytesGlobal;
|
|
38577
|
+
maxBytesPerActor;
|
|
38578
|
+
maxBytesPerSession;
|
|
38579
|
+
maxGenerationsPerSession;
|
|
38580
|
+
maxRecordBytes;
|
|
38581
|
+
maxSessionsGlobal;
|
|
38582
|
+
maxSessionsPerActor;
|
|
38583
|
+
async cleanupExpiredSessionRows() {
|
|
38584
|
+
const page = await this.state.list(FEDERATED_SEARCH_SESSION_KEY_PREFIX, { limit: 32 });
|
|
38585
|
+
for (const item of page.items) {
|
|
38586
|
+
let record2;
|
|
38587
|
+
try {
|
|
38588
|
+
record2 = parseRecord(item.value);
|
|
38589
|
+
} catch {
|
|
38590
|
+
continue;
|
|
38591
|
+
}
|
|
38592
|
+
if (Date.parse(record2.expiresAt) <= this.now()) {
|
|
38593
|
+
await this.state.delete(item.key).catch(() => {
|
|
38594
|
+
});
|
|
38595
|
+
}
|
|
38596
|
+
}
|
|
38597
|
+
}
|
|
38598
|
+
async reserveQuota(handle, record2, recordBytes) {
|
|
38599
|
+
const id = await sha256Hex(record2.sessionId);
|
|
38600
|
+
const generationId = await sha256Hex(handle);
|
|
38601
|
+
for (let attempt2 = 0; attempt2 < QUOTA_CAS_ATTEMPTS; attempt2++) {
|
|
38602
|
+
const raw = await this.state.get(FEDERATED_SEARCH_QUOTA_KEY);
|
|
38603
|
+
const current = raw === null ? { revision: 0, sessions: [] } : parseQuotaRecord(raw);
|
|
38604
|
+
const active = current.sessions.filter((entry) => Date.parse(entry.expiresAt) > this.now());
|
|
38605
|
+
const existing = active.find((entry) => entry.id === id);
|
|
38606
|
+
let nextEntry;
|
|
38607
|
+
if (existing !== void 0) {
|
|
38608
|
+
if (existing.actorKeyId !== record2.actorKeyId || existing.expiresAt !== record2.expiresAt) throw invalidSession();
|
|
38609
|
+
if (record2.generation === existing.generation) {
|
|
38610
|
+
if (existing.generationId !== generationId || existing.generationBytes !== recordBytes) throw invalidSession();
|
|
38611
|
+
return;
|
|
38612
|
+
}
|
|
38613
|
+
if (record2.generation !== existing.generation + 1 || generationId === existing.generationId) throw invalidSession();
|
|
38614
|
+
nextEntry = {
|
|
38615
|
+
...existing,
|
|
38616
|
+
bytes: existing.bytes + recordBytes,
|
|
38617
|
+
generation: record2.generation,
|
|
38618
|
+
generationBytes: recordBytes,
|
|
38619
|
+
generationId
|
|
38620
|
+
};
|
|
38621
|
+
} else {
|
|
38622
|
+
if (record2.generation !== 0) throw invalidSession();
|
|
38623
|
+
nextEntry = {
|
|
38624
|
+
actorKeyId: record2.actorKeyId,
|
|
38625
|
+
bytes: recordBytes,
|
|
38626
|
+
expiresAt: record2.expiresAt,
|
|
38627
|
+
generation: 0,
|
|
38628
|
+
generationBytes: recordBytes,
|
|
38629
|
+
generationId,
|
|
38630
|
+
id
|
|
38631
|
+
};
|
|
38632
|
+
}
|
|
38633
|
+
const actorCount = active.filter((entry) => entry.actorKeyId === record2.actorKeyId).length;
|
|
38634
|
+
const globalBytes = active.reduce((total, entry) => total + entry.bytes, 0);
|
|
38635
|
+
const actorBytes = active.filter((entry) => entry.actorKeyId === record2.actorKeyId).reduce((total, entry) => total + entry.bytes, 0);
|
|
38636
|
+
const priorBytes = existing?.bytes ?? 0;
|
|
38637
|
+
if (record2.generation >= this.maxGenerationsPerSession || nextEntry.bytes > this.maxBytesPerSession) {
|
|
38638
|
+
throw new TBError("rate_limited", "federated search session chain quota exceeded", {
|
|
38639
|
+
retryable: false
|
|
38640
|
+
});
|
|
38641
|
+
}
|
|
38642
|
+
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) {
|
|
38643
|
+
throw new TBError("rate_limited", "federated search session quota exceeded", {
|
|
38644
|
+
retryable: true
|
|
38645
|
+
});
|
|
38646
|
+
}
|
|
38647
|
+
const next = {
|
|
38648
|
+
revision: raw === null ? 0 : current.revision + 1,
|
|
38649
|
+
sessions: [
|
|
38650
|
+
...active.filter((entry) => entry.id !== id),
|
|
38651
|
+
nextEntry
|
|
38652
|
+
]
|
|
38653
|
+
};
|
|
38654
|
+
if (await this.cas(
|
|
38655
|
+
FEDERATED_SEARCH_QUOTA_KEY,
|
|
38656
|
+
raw === null ? null : current.revision,
|
|
38657
|
+
next
|
|
38658
|
+
)) return;
|
|
38659
|
+
}
|
|
38660
|
+
throw new TBError("unavailable", "federated search session quota is busy", {
|
|
38661
|
+
retryable: true
|
|
38662
|
+
});
|
|
38663
|
+
}
|
|
38664
|
+
issueHandle() {
|
|
38665
|
+
const bytes = this.randomBytes(FEDERATED_SEARCH_HANDLE_RANDOM_BYTES);
|
|
38666
|
+
if (!(bytes instanceof Uint8Array) || bytes.length !== FEDERATED_SEARCH_HANDLE_RANDOM_BYTES) {
|
|
38667
|
+
throw sessionUnavailable("federated search session randomness is unavailable");
|
|
38668
|
+
}
|
|
38669
|
+
const handle = FEDERATED_SEARCH_HANDLE_PREFIX + base64urlEncode(bytes);
|
|
38670
|
+
if (!isFederatedSearchSessionHandle(handle)) {
|
|
38671
|
+
throw sessionUnavailable("federated search session randomness is invalid");
|
|
38672
|
+
}
|
|
38673
|
+
return handle;
|
|
38674
|
+
}
|
|
38675
|
+
async create(handle, input) {
|
|
38676
|
+
const key = await federatedSearchSessionStateKey(handle);
|
|
38677
|
+
const record2 = parseCreate(handle, input);
|
|
38678
|
+
if (Date.parse(record2.expiresAt) <= this.now()) throw invalidSession();
|
|
38679
|
+
const recordBytes = new TextEncoder().encode(JSON.stringify(record2)).length;
|
|
38680
|
+
if (recordBytes > this.maxRecordBytes) {
|
|
38681
|
+
throw new TBError("rate_limited", "federated search session record is too large", {
|
|
38682
|
+
retryable: false
|
|
38683
|
+
});
|
|
38684
|
+
}
|
|
38685
|
+
if (await this.state.get(key) !== null) return await this.read(handle, input);
|
|
38686
|
+
if (record2.generation === 0) await this.cleanupExpiredSessionRows();
|
|
38687
|
+
await this.reserveQuota(handle, record2, recordBytes);
|
|
38688
|
+
if (await this.cas(key, null, record2)) {
|
|
38689
|
+
return record2;
|
|
38690
|
+
}
|
|
38691
|
+
return await this.read(handle, input);
|
|
38692
|
+
}
|
|
38693
|
+
async createNew(input) {
|
|
38694
|
+
const handle = this.issueHandle();
|
|
38695
|
+
return { handle, record: await this.create(handle, input) };
|
|
38696
|
+
}
|
|
38697
|
+
async read(handle, binding) {
|
|
38698
|
+
if (!validOpaque(binding.actorKeyId) || !validOpaque(binding.requestDigest)) {
|
|
38699
|
+
throw invalidSession();
|
|
38700
|
+
}
|
|
38701
|
+
const raw = await this.state.get(await federatedSearchSessionStateKey(handle));
|
|
38702
|
+
if (raw === null) throw invalidSession();
|
|
38703
|
+
const record2 = parseRecord(raw);
|
|
38704
|
+
if (Date.parse(record2.expiresAt) <= this.now()) {
|
|
38705
|
+
await this.state.delete(await federatedSearchSessionStateKey(handle)).catch(() => {
|
|
38706
|
+
});
|
|
38707
|
+
throw invalidSession();
|
|
38708
|
+
}
|
|
38709
|
+
if (record2.actorKeyId !== binding.actorKeyId || record2.requestDigest !== binding.requestDigest) {
|
|
38710
|
+
throw invalidSession();
|
|
38711
|
+
}
|
|
38712
|
+
return record2;
|
|
38713
|
+
}
|
|
38714
|
+
};
|
|
38715
|
+
|
|
38716
|
+
// ../app/src/search/localSearch.ts
|
|
38717
|
+
function normalizedEffect(effect) {
|
|
38718
|
+
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
38719
|
+
}
|
|
38720
|
+
function projectSearchTool(tool, detail) {
|
|
38721
|
+
if (detail === "full") return tool;
|
|
38722
|
+
const compact = { ...tool };
|
|
38723
|
+
delete compact.inputSchema;
|
|
38724
|
+
delete compact.outputSchema;
|
|
38725
|
+
if (compact.description !== void 0) {
|
|
38726
|
+
const encoder = new TextEncoder();
|
|
38727
|
+
if (encoder.encode(compact.description).length > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) {
|
|
38728
|
+
let description = "";
|
|
38729
|
+
let bytes = 0;
|
|
38730
|
+
for (const char of compact.description) {
|
|
38731
|
+
const size = encoder.encode(char).length;
|
|
38732
|
+
if (bytes + size > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) break;
|
|
38733
|
+
description += char;
|
|
38734
|
+
bytes += size;
|
|
38735
|
+
}
|
|
38736
|
+
compact.description = description;
|
|
38737
|
+
}
|
|
38738
|
+
}
|
|
38739
|
+
return compact;
|
|
38740
|
+
}
|
|
38741
|
+
async function executeLocalSearch(deps, request) {
|
|
38742
|
+
const { ctx, search, searchSync, state } = deps;
|
|
38743
|
+
const query = request.query;
|
|
38744
|
+
const wireOpts = request.opts ?? {};
|
|
38745
|
+
const detail = wireOpts.detail ?? "compact";
|
|
38746
|
+
const matching = wireOpts.matching ?? "best";
|
|
38747
|
+
const mode = wireOpts.mode ?? "keyword";
|
|
38748
|
+
const limit = normalizeToolSearchLimit(wireOpts.limit);
|
|
38749
|
+
const constraints = normalizeToolSearchOptions({
|
|
38750
|
+
...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
|
|
38751
|
+
matching,
|
|
38752
|
+
...wireOpts.minCoverage === void 0 ? {} : { minCoverage: wireOpts.minCoverage },
|
|
38753
|
+
...wireOpts.pathPrefix === void 0 ? {} : { pathPrefix: wireOpts.pathPrefix }
|
|
38754
|
+
});
|
|
38755
|
+
const indexOptions = { mode, ...constraints };
|
|
38756
|
+
await searchSync?.ensureReady();
|
|
38757
|
+
const registry = new NodeRegistryStore(state);
|
|
38758
|
+
const selected = [];
|
|
38759
|
+
const workLimit = Math.min(
|
|
38760
|
+
TOOL_SEARCH_WORK_LIMIT,
|
|
38761
|
+
Math.max(TOOL_SEARCH_BATCH_LIMIT, limit * 2)
|
|
38762
|
+
);
|
|
38763
|
+
let scanCursor = wireOpts.cursor;
|
|
38764
|
+
let responseCursor;
|
|
38765
|
+
let responseCandidate;
|
|
38766
|
+
let visibleCoverage;
|
|
38767
|
+
let scanned = 0;
|
|
38768
|
+
let stopped = false;
|
|
38769
|
+
while (!stopped && selected.length < limit && scanned < workLimit) {
|
|
38770
|
+
const batchLimit = Math.min(TOOL_SEARCH_BATCH_LIMIT, workLimit - scanned);
|
|
38771
|
+
const page = await search.search(query, {
|
|
38772
|
+
...indexOptions,
|
|
38773
|
+
limit: batchLimit,
|
|
38774
|
+
...scanCursor === void 0 ? {} : { cursor: scanCursor }
|
|
38775
|
+
});
|
|
38776
|
+
if (page.items.length === 0) {
|
|
38777
|
+
responseCursor = page.cursor;
|
|
38778
|
+
break;
|
|
38779
|
+
}
|
|
38780
|
+
const nodes = await registry.getMany(page.items.map((candidate) => candidate.path));
|
|
38781
|
+
let lastCandidateInVisibleBand;
|
|
38782
|
+
for (const [index, candidate] of page.items.entries()) {
|
|
38783
|
+
if (matching === "best" && visibleCoverage !== void 0 && candidate.coverage !== visibleCoverage) {
|
|
38784
|
+
responseCandidate = lastCandidateInVisibleBand;
|
|
38785
|
+
responseCursor = responseCandidate === void 0 ? scanCursor : void 0;
|
|
38786
|
+
stopped = true;
|
|
38787
|
+
break;
|
|
38788
|
+
}
|
|
38789
|
+
scanned++;
|
|
38790
|
+
lastCandidateInVisibleBand = candidate;
|
|
38791
|
+
if (!check(ctx, candidate.path, "read").allow || !check(ctx, candidate.path, "call").allow) {
|
|
38792
|
+
continue;
|
|
38793
|
+
}
|
|
38794
|
+
const node = nodes.get(candidate.path);
|
|
38795
|
+
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) {
|
|
38796
|
+
continue;
|
|
38797
|
+
}
|
|
38798
|
+
visibleCoverage ??= candidate.coverage;
|
|
38799
|
+
selected.push({ candidate, node });
|
|
38800
|
+
if (selected.length === limit) {
|
|
38801
|
+
const hasUnscanned = index < page.items.length - 1 || page.cursor !== void 0;
|
|
38802
|
+
responseCandidate = hasUnscanned ? candidate : void 0;
|
|
38803
|
+
responseCursor = void 0;
|
|
38804
|
+
stopped = true;
|
|
38805
|
+
break;
|
|
38806
|
+
}
|
|
38807
|
+
}
|
|
38808
|
+
if (stopped) break;
|
|
38809
|
+
responseCursor = page.cursor;
|
|
38810
|
+
if (page.cursor === void 0) break;
|
|
38811
|
+
scanCursor = page.cursor;
|
|
38812
|
+
}
|
|
38813
|
+
const canonicalTools = await canonicalSearchTools(state, selected.map((item) => item.node));
|
|
38814
|
+
const requestedEffects = constraints.effects === void 0 ? void 0 : new Set(constraints.effects);
|
|
38815
|
+
const items = [];
|
|
38816
|
+
let pageBytes = 0;
|
|
38817
|
+
for (const [index, selectedItem] of selected.entries()) {
|
|
38818
|
+
const raw = canonicalTools.get(selectedItem.candidate.path)?.find((tool2) => tool2.name === selectedItem.candidate.name);
|
|
38819
|
+
if (raw === void 0) continue;
|
|
38820
|
+
const tool = virtualizeTools(selectedItem.node.virtualize, [raw]).exposed[0];
|
|
38821
|
+
if (tool === void 0) continue;
|
|
38822
|
+
if (requestedEffects !== void 0 && !requestedEffects.has(normalizedEffect(tool.effect))) {
|
|
38823
|
+
continue;
|
|
38824
|
+
}
|
|
38825
|
+
const item = {
|
|
38826
|
+
path: selectedItem.candidate.path,
|
|
38827
|
+
relevance: {
|
|
38828
|
+
coverage: selectedItem.candidate.coverage,
|
|
38829
|
+
matchedTermCount: selectedItem.candidate.matchedTermCount,
|
|
38830
|
+
rankingVersion: "keyword-v2",
|
|
38831
|
+
totalTermCount: selectedItem.candidate.totalTermCount
|
|
38832
|
+
},
|
|
38833
|
+
tool: projectSearchTool(tool, detail)
|
|
38834
|
+
};
|
|
38835
|
+
const itemBytes = new TextEncoder().encode(JSON.stringify(item)).length;
|
|
38836
|
+
if (pageBytes + itemBytes > TOOL_SEARCH_PAGE_BYTES) {
|
|
38837
|
+
if (items.length === 0) {
|
|
38838
|
+
throw new TBError("internal", "\u5DE5\u5177\u641C\u7D22\u9875\u9762\u5B57\u8282\u9884\u7B97\u65E0\u6CD5\u5BB9\u7EB3\u9996\u4E2A\u7ED3\u679C");
|
|
38839
|
+
}
|
|
38840
|
+
responseCandidate = selected[index - 1]?.candidate;
|
|
38841
|
+
responseCursor = void 0;
|
|
38842
|
+
break;
|
|
38843
|
+
}
|
|
38844
|
+
items.push(item);
|
|
38845
|
+
pageBytes += itemBytes;
|
|
38846
|
+
}
|
|
38847
|
+
if (responseCandidate !== void 0) {
|
|
38848
|
+
responseCursor = await search.cursorFor(query, responseCandidate, mode);
|
|
38849
|
+
}
|
|
38850
|
+
if (items.length === 0) responseCursor = void 0;
|
|
38851
|
+
return responseCursor === void 0 ? { items } : { items, cursor: responseCursor };
|
|
38852
|
+
}
|
|
38853
|
+
|
|
38854
|
+
// ../app/src/search/federatedSearch.ts
|
|
38855
|
+
var HEADER_REMAINING_MS = "x-tb-search-remaining-ms";
|
|
38856
|
+
var HEADER_SESSION_TTL_MS = "x-tb-search-session-ttl-ms";
|
|
38857
|
+
var HEADER_SOURCE_BUDGET = "x-tb-search-source-budget";
|
|
38858
|
+
var HEADER_VALIDATE_SNAPSHOT = "x-tb-search-validate-snapshot";
|
|
38859
|
+
var HEADER_VIA = "x-tb-via";
|
|
38860
|
+
var HEADER_WANT_SNAPSHOT = "x-tb-search-want-snapshot";
|
|
38861
|
+
function clampHeader(value, local) {
|
|
38862
|
+
if (value === null || !/^\d+$/u.test(value)) return local;
|
|
38863
|
+
const parsed = Number(value);
|
|
38864
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? Math.min(parsed, local) : local;
|
|
38865
|
+
}
|
|
38866
|
+
function isPrefix(prefix, path) {
|
|
38867
|
+
return prefix === "" || prefix === path || path.startsWith(`${prefix}/`);
|
|
38868
|
+
}
|
|
38869
|
+
function controlledStatus(error) {
|
|
38870
|
+
if (error instanceof TBError) {
|
|
38871
|
+
if (error.message.includes("\u73AF")) return "cycle";
|
|
38872
|
+
if (error.message.includes("\u8DF3\u6570")) return "hop_limit";
|
|
38873
|
+
if (error.message.includes("aborted")) return "timed_out";
|
|
38874
|
+
if (error.message.includes("work budget")) return "budget_exhausted";
|
|
38875
|
+
}
|
|
38876
|
+
return "unavailable";
|
|
38877
|
+
}
|
|
38878
|
+
function retryableContinuationError() {
|
|
38879
|
+
return new TBError("unavailable", "federated search continuation source unavailable", {
|
|
38880
|
+
retryable: true
|
|
38881
|
+
});
|
|
38882
|
+
}
|
|
38883
|
+
function invalidContinuation() {
|
|
38884
|
+
return new TBError("invalid_argument", "federated search cursor no longer matches topology", {
|
|
38885
|
+
retryable: false
|
|
38886
|
+
});
|
|
38887
|
+
}
|
|
38888
|
+
function continuationSourceError(error) {
|
|
38889
|
+
if (error instanceof TBError) {
|
|
38890
|
+
if (error.code === "invalid_argument") return error;
|
|
38891
|
+
if (error.message.includes("source work budget exhausted")) return invalidContinuation();
|
|
38892
|
+
}
|
|
38893
|
+
return retryableContinuationError();
|
|
38894
|
+
}
|
|
38895
|
+
function compactItem(item) {
|
|
38896
|
+
const tool = { ...item.tool };
|
|
38897
|
+
delete tool.inputSchema;
|
|
38898
|
+
delete tool.outputSchema;
|
|
38899
|
+
if (tool.description !== void 0) {
|
|
38900
|
+
const encoder = new TextEncoder();
|
|
38901
|
+
if (encoder.encode(tool.description).length > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) {
|
|
38902
|
+
let description = "";
|
|
38903
|
+
let bytes = 0;
|
|
38904
|
+
for (const char of tool.description) {
|
|
38905
|
+
const size = encoder.encode(char).length;
|
|
38906
|
+
if (bytes + size > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) break;
|
|
38907
|
+
description += char;
|
|
38908
|
+
bytes += size;
|
|
38909
|
+
}
|
|
38910
|
+
tool.description = description;
|
|
38911
|
+
}
|
|
38912
|
+
}
|
|
38913
|
+
return { ...item, tool };
|
|
38914
|
+
}
|
|
38915
|
+
function normalizedEffect2(effect) {
|
|
38916
|
+
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
38917
|
+
}
|
|
38918
|
+
function requestConstraints(request) {
|
|
38919
|
+
return normalizeToolSearchOptions({
|
|
38920
|
+
...request.opts?.effects === void 0 ? {} : { effects: request.opts.effects },
|
|
38921
|
+
matching: request.opts?.matching ?? "best",
|
|
38922
|
+
...request.opts?.minCoverage === void 0 ? {} : { minCoverage: request.opts.minCoverage },
|
|
38923
|
+
...request.opts?.pathPrefix === void 0 ? {} : { pathPrefix: request.opts.pathPrefix }
|
|
38924
|
+
});
|
|
38925
|
+
}
|
|
38926
|
+
function assertHardResultConstraints(item, constraints) {
|
|
38927
|
+
const effects = constraints.effects === void 0 ? void 0 : new Set(constraints.effects);
|
|
38928
|
+
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))) {
|
|
38929
|
+
throw new TBError("unavailable", "remote search violated a hard result constraint", {
|
|
38930
|
+
retryable: false
|
|
38931
|
+
});
|
|
38932
|
+
}
|
|
38933
|
+
}
|
|
38934
|
+
function sourceStatusKey(value) {
|
|
38935
|
+
return `${value.path}\0${value.status}`;
|
|
38936
|
+
}
|
|
38937
|
+
function mergeStatuses(...groups) {
|
|
38938
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38939
|
+
const out = [];
|
|
38940
|
+
for (const item of groups.flat()) {
|
|
38941
|
+
const key = sourceStatusKey(item);
|
|
38942
|
+
if (seen.has(key)) continue;
|
|
38943
|
+
seen.add(key);
|
|
38944
|
+
out.push(item);
|
|
38945
|
+
}
|
|
38946
|
+
return out.sort((left, right) => left.path.localeCompare(right.path) || left.status.localeCompare(right.status));
|
|
38947
|
+
}
|
|
38948
|
+
function responseHasMore(sources) {
|
|
38949
|
+
return sources.some((source) => source.pending !== void 0 || !source.done);
|
|
38950
|
+
}
|
|
38951
|
+
function serializableSources(sources) {
|
|
38952
|
+
return sources.map((source) => ({
|
|
38953
|
+
balanceCount: source.balanceCount,
|
|
38954
|
+
...source.balanceCoverage === void 0 ? {} : { balanceCoverage: source.balanceCoverage },
|
|
38955
|
+
budget: source.budget,
|
|
38956
|
+
done: source.done,
|
|
38957
|
+
...source.excluded === void 0 ? {} : { excluded: source.excluded },
|
|
38958
|
+
kind: source.kind,
|
|
38959
|
+
path: source.path,
|
|
38960
|
+
...source.cursor === void 0 ? {} : { cursor: source.cursor },
|
|
38961
|
+
...source.nextCursor === void 0 ? {} : { nextCursor: source.nextCursor },
|
|
38962
|
+
...source.pending === void 0 ? {} : { pending: compactItem(source.pending) },
|
|
38963
|
+
...source.validationCursor === void 0 ? {} : { validationCursor: source.validationCursor }
|
|
38964
|
+
}));
|
|
38965
|
+
}
|
|
38966
|
+
function canonicalJson(value) {
|
|
38967
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
38968
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
38969
|
+
const object2 = value;
|
|
38970
|
+
return `{${Object.keys(object2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object2[key])}`).join(",")}}`;
|
|
38971
|
+
}
|
|
38972
|
+
function sameJson(left, right) {
|
|
38973
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
38974
|
+
}
|
|
38975
|
+
function assertNextRecord(current, next, topology) {
|
|
38976
|
+
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)) {
|
|
38977
|
+
throw invalidContinuation();
|
|
38978
|
+
}
|
|
38979
|
+
}
|
|
38980
|
+
function assertCachedPage(computed, cached) {
|
|
38981
|
+
if (!sameJson(computed.page, cached.page) || !sameJson(computed.excluded, cached.excludedStatuses) || !sameJson(serializableSources(computed.sources), cached.sourceContinuations)) {
|
|
38982
|
+
throw invalidContinuation();
|
|
38983
|
+
}
|
|
38984
|
+
}
|
|
38985
|
+
function requestForDigest(request, limit) {
|
|
38986
|
+
const opts = request.opts ?? {};
|
|
38987
|
+
return {
|
|
38988
|
+
query: request.query,
|
|
38989
|
+
opts: {
|
|
38990
|
+
effects: opts.effects ?? null,
|
|
38991
|
+
federation: "recursive",
|
|
38992
|
+
limit,
|
|
38993
|
+
matching: opts.matching ?? "best",
|
|
38994
|
+
minCoverage: opts.minCoverage ?? null,
|
|
38995
|
+
mode: opts.mode ?? "keyword",
|
|
38996
|
+
pathPrefix: opts.pathPrefix ?? null
|
|
38997
|
+
}
|
|
38998
|
+
};
|
|
38999
|
+
}
|
|
39000
|
+
async function topologyDigest(nodes, includeLocal, localRevision, remotePolicy) {
|
|
39001
|
+
return await sha256Hex(canonicalJson({
|
|
39002
|
+
local: includeLocal,
|
|
39003
|
+
localRevision,
|
|
39004
|
+
nodes: nodes.map((node) => ({
|
|
39005
|
+
kind: node.kind,
|
|
39006
|
+
path: node.path,
|
|
39007
|
+
remote: node.config?.kind === "remote" ? { baseUrl: node.config.baseUrl, skRef: node.config.skRef ?? null } : null,
|
|
39008
|
+
updatedAt: node.updatedAt ?? "",
|
|
39009
|
+
virtualize: node.virtualize ?? null
|
|
39010
|
+
})),
|
|
39011
|
+
remotePolicy
|
|
39012
|
+
}));
|
|
39013
|
+
}
|
|
39014
|
+
function directRemoteNodes(nodes) {
|
|
39015
|
+
const remotes = nodes.filter((node) => node.kind === "remote" && node.config?.kind === "remote").sort((left, right) => left.path.localeCompare(right.path));
|
|
39016
|
+
return remotes.filter((node) => !remotes.some((parent) => parent.path !== node.path && isPrefix(parent.path, node.path)));
|
|
39017
|
+
}
|
|
39018
|
+
function childPrefixFor(mount, pathPrefix) {
|
|
39019
|
+
if (pathPrefix === void 0) return { eligible: true };
|
|
39020
|
+
if (isPrefix(mount, pathPrefix)) {
|
|
39021
|
+
const child = new RemotePathProjector(mount).childPath(pathPrefix);
|
|
39022
|
+
return { eligible: true, ...child === "" ? {} : { pathPrefix: child } };
|
|
39023
|
+
}
|
|
39024
|
+
return { eligible: isPrefix(pathPrefix, mount) };
|
|
39025
|
+
}
|
|
39026
|
+
function localEligible(remotes, pathPrefix) {
|
|
39027
|
+
return pathPrefix === void 0 || !remotes.some((node) => isPrefix(node.path, pathPrefix));
|
|
39028
|
+
}
|
|
39029
|
+
async function mapLimit(values, concurrency, fn) {
|
|
39030
|
+
const results = new Array(values.length);
|
|
39031
|
+
let next = 0;
|
|
39032
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
39033
|
+
while (next < values.length) {
|
|
39034
|
+
const index = next++;
|
|
39035
|
+
results[index] = await fn(values[index]);
|
|
39036
|
+
}
|
|
39037
|
+
});
|
|
39038
|
+
await Promise.all(workers);
|
|
39039
|
+
return results;
|
|
39040
|
+
}
|
|
39041
|
+
function deadlineSignal(deadline) {
|
|
39042
|
+
return AbortSignal.timeout(Math.max(1, Math.floor(deadline - Date.now())));
|
|
39043
|
+
}
|
|
39044
|
+
function federatedHeaders(inbound, remainingMs, sourceBudget, sessionTtlMs) {
|
|
39045
|
+
const headers = new Headers();
|
|
39046
|
+
headers.set("accept", "application/json");
|
|
39047
|
+
headers.set("content-type", "application/json");
|
|
39048
|
+
const via = inbound.get(HEADER_VIA);
|
|
39049
|
+
if (via !== null) headers.set(HEADER_VIA, via);
|
|
39050
|
+
headers.set(HEADER_REMAINING_MS, String(Math.max(1, Math.floor(remainingMs))));
|
|
39051
|
+
headers.set(HEADER_SESSION_TTL_MS, String(Math.max(1, Math.floor(sessionTtlMs))));
|
|
39052
|
+
headers.set(HEADER_SOURCE_BUDGET, String(Math.max(1, Math.floor(sourceBudget))));
|
|
39053
|
+
headers.set(HEADER_WANT_SNAPSHOT, "1");
|
|
39054
|
+
return headers;
|
|
39055
|
+
}
|
|
39056
|
+
function projectChildPage(mount, page, totalTermCount, ctx, request) {
|
|
39057
|
+
const projector = new RemotePathProjector(mount);
|
|
39058
|
+
const constraints = requestConstraints(request);
|
|
39059
|
+
const items = page.items.map((item) => {
|
|
39060
|
+
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) {
|
|
39061
|
+
throw new TBError("unavailable", "remote search returned incompatible relevance evidence", {
|
|
39062
|
+
retryable: false
|
|
39063
|
+
});
|
|
39064
|
+
}
|
|
39065
|
+
const path = projector.localPath(item.path, false);
|
|
39066
|
+
const sourcePath = projector.localPath(item.source?.path ?? "", true);
|
|
39067
|
+
if (item.tool.name.includes("/")) {
|
|
39068
|
+
throw new TBError("unavailable", "remote search returned an unsafe tool name", {
|
|
39069
|
+
retryable: false
|
|
39070
|
+
});
|
|
39071
|
+
}
|
|
39072
|
+
try {
|
|
39073
|
+
if (canonicalRemotePath(item.tool.name, false) !== item.tool.name.toLowerCase()) {
|
|
39074
|
+
throw new Error("non-canonical");
|
|
39075
|
+
}
|
|
39076
|
+
} catch {
|
|
39077
|
+
throw new TBError("unavailable", "remote search returned an unsafe tool name", {
|
|
39078
|
+
retryable: false
|
|
39079
|
+
});
|
|
39080
|
+
}
|
|
39081
|
+
if (!isPrefix(sourcePath, path)) {
|
|
39082
|
+
throw new TBError("unavailable", "remote search source does not own its result", {
|
|
39083
|
+
retryable: false
|
|
39084
|
+
});
|
|
39085
|
+
}
|
|
39086
|
+
const projected = compactItem({ ...item, path, source: { path: sourcePath } });
|
|
39087
|
+
assertHardResultConstraints(projected, constraints);
|
|
39088
|
+
return projected;
|
|
39089
|
+
}).filter((item) => check(ctx, item.path, "read").allow && check(ctx, item.path, "call").allow);
|
|
39090
|
+
const projectedSources = page.sources?.map((source) => ({
|
|
39091
|
+
path: projector.localPath(source.path, true),
|
|
39092
|
+
status: source.status
|
|
39093
|
+
}));
|
|
39094
|
+
const sourceStatuses = /* @__PURE__ */ new Map();
|
|
39095
|
+
for (const source of projectedSources ?? []) {
|
|
39096
|
+
const previous = sourceStatuses.get(source.path);
|
|
39097
|
+
if (previous !== void 0 && previous !== source.status) {
|
|
39098
|
+
throw new TBError("unavailable", "remote search returned conflicting source statuses", {
|
|
39099
|
+
retryable: false
|
|
39100
|
+
});
|
|
39101
|
+
}
|
|
39102
|
+
sourceStatuses.set(source.path, source.status);
|
|
39103
|
+
}
|
|
39104
|
+
const hasFailure = [...sourceStatuses.values()].some((status) => status !== "ok");
|
|
39105
|
+
if (page.partial === true !== hasFailure || page.cursor !== void 0 && !isFederatedSearchSessionHandle(page.cursor) || page.snapshot !== void 0 && !isFederatedSearchSessionHandle(page.snapshot)) {
|
|
39106
|
+
throw new TBError("unavailable", "remote search returned inconsistent page metadata", {
|
|
39107
|
+
retryable: false
|
|
39108
|
+
});
|
|
39109
|
+
}
|
|
39110
|
+
const sources = projectedSources?.filter((source) => check(ctx, source.path, "read").allow && check(ctx, source.path, "call").allow);
|
|
39111
|
+
const visiblePartial = sources?.some((source) => source.status !== "ok") === true;
|
|
39112
|
+
return {
|
|
39113
|
+
items,
|
|
39114
|
+
...page.cursor === void 0 ? {} : { cursor: page.cursor },
|
|
39115
|
+
...visiblePartial ? { partial: true } : {},
|
|
39116
|
+
...sources === void 0 ? {} : { sources }
|
|
39117
|
+
};
|
|
39118
|
+
}
|
|
39119
|
+
async function remoteResponse(deps, input, node, requestPath, method, headers, deadline, body) {
|
|
39120
|
+
if (node.config?.kind !== "remote") throw invalidContinuation();
|
|
39121
|
+
return await passthroughRemote({
|
|
39122
|
+
actor: {
|
|
39123
|
+
keyId: input.ctx.keyId,
|
|
39124
|
+
owner: input.ctx.owner,
|
|
39125
|
+
traceId: input.ctx.traceId
|
|
39126
|
+
},
|
|
39127
|
+
...body === void 0 ? {} : { body },
|
|
39128
|
+
config: node.config,
|
|
39129
|
+
headers,
|
|
39130
|
+
maxResponseBodyBytes: federatedSearchSettings(deps.remote).maxResponseBodyBytes,
|
|
39131
|
+
method,
|
|
39132
|
+
nodePath: node.path,
|
|
39133
|
+
requestPath,
|
|
39134
|
+
requestUrl: input.requestUrl,
|
|
39135
|
+
secrets: deps.secrets,
|
|
39136
|
+
settings: await resolveRemoteSettings(input.state, deps.remote),
|
|
39137
|
+
signal: deadlineSignal(deadline)
|
|
39138
|
+
});
|
|
39139
|
+
}
|
|
39140
|
+
async function assertFederatedCapability(deps, input, node, headers, deadline) {
|
|
39141
|
+
if (node.config?.kind !== "remote" || node.config.skRef === void 0) {
|
|
39142
|
+
throw new TBError("unavailable", "remote search source is unsupported", { retryable: false });
|
|
39143
|
+
}
|
|
39144
|
+
const response = await remoteResponse(
|
|
39145
|
+
deps,
|
|
39146
|
+
input,
|
|
39147
|
+
node,
|
|
39148
|
+
`${node.path}/~describe`,
|
|
39149
|
+
"GET",
|
|
39150
|
+
headers,
|
|
39151
|
+
deadline
|
|
39152
|
+
);
|
|
39153
|
+
if (!response.ok) {
|
|
39154
|
+
throw new TBError("unavailable", "remote search capability is unavailable", {
|
|
39155
|
+
retryable: response.status >= 500
|
|
39156
|
+
});
|
|
39157
|
+
}
|
|
39158
|
+
const value = await response.json().catch(() => null);
|
|
39159
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.capabilities) || !value.capabilities.includes("search:federated")) {
|
|
39160
|
+
throw new TBError("unavailable", "remote search source is unsupported", { retryable: false });
|
|
39161
|
+
}
|
|
39162
|
+
}
|
|
39163
|
+
function childSearchRequest(input, source, cursor) {
|
|
39164
|
+
const opts = input.request.opts ?? {};
|
|
39165
|
+
const childPrefix = childPrefixFor(source.path, opts.pathPrefix).pathPrefix;
|
|
39166
|
+
return {
|
|
39167
|
+
query: input.request.query,
|
|
39168
|
+
opts: {
|
|
39169
|
+
...opts.effects === void 0 ? {} : { effects: opts.effects },
|
|
39170
|
+
...cursor === void 0 ? {} : { cursor },
|
|
39171
|
+
detail: "compact",
|
|
39172
|
+
federation: "recursive",
|
|
39173
|
+
limit: 1,
|
|
39174
|
+
matching: opts.matching ?? "best",
|
|
39175
|
+
...opts.minCoverage === void 0 ? {} : { minCoverage: opts.minCoverage },
|
|
39176
|
+
mode: "keyword",
|
|
39177
|
+
...childPrefix === void 0 ? {} : { pathPrefix: childPrefix }
|
|
39178
|
+
}
|
|
39179
|
+
};
|
|
39180
|
+
}
|
|
39181
|
+
async function throwChildSearchError(response) {
|
|
39182
|
+
const error = await response.json().catch(() => null);
|
|
39183
|
+
const message = typeof error?.message === "string" ? error.message : "";
|
|
39184
|
+
if (message.includes("\u73AF")) {
|
|
39185
|
+
throw new TBError("unavailable", "\u68C0\u6D4B\u5230\u900F\u4F20\u73AF", { retryable: false });
|
|
39186
|
+
}
|
|
39187
|
+
if (message.includes("\u8DF3\u6570")) {
|
|
39188
|
+
throw new TBError("unavailable", "\u900F\u4F20\u8DF3\u6570\u8D85\u9650", { retryable: false });
|
|
39189
|
+
}
|
|
39190
|
+
if (response.status >= 400 && response.status < 500) throw invalidContinuation();
|
|
39191
|
+
throw new TBError("unavailable", "remote search unavailable", {
|
|
39192
|
+
retryable: response.status >= 500
|
|
39193
|
+
});
|
|
39194
|
+
}
|
|
39195
|
+
async function fetchHead(deps, input, source, remoteNodes, deadline, expiresAt, totalTermCount, skipBudget = 20) {
|
|
39196
|
+
if (source.done || source.pending !== void 0) return { statuses: [] };
|
|
39197
|
+
const opts = input.request.opts ?? {};
|
|
39198
|
+
if (source.kind === "local") {
|
|
39199
|
+
const page2 = await executeLocalSearch(
|
|
39200
|
+
{
|
|
39201
|
+
ctx: input.ctx,
|
|
39202
|
+
search: input.search,
|
|
39203
|
+
state: input.state
|
|
39204
|
+
},
|
|
39205
|
+
{
|
|
39206
|
+
query: input.request.query,
|
|
39207
|
+
opts: {
|
|
39208
|
+
...opts.effects === void 0 ? {} : { effects: opts.effects },
|
|
39209
|
+
...source.cursor === void 0 ? {} : { cursor: source.cursor },
|
|
39210
|
+
detail: "compact",
|
|
39211
|
+
federation: "local",
|
|
39212
|
+
limit: 1,
|
|
39213
|
+
matching: opts.matching ?? "best",
|
|
39214
|
+
...opts.minCoverage === void 0 ? {} : { minCoverage: opts.minCoverage },
|
|
39215
|
+
mode: "keyword",
|
|
39216
|
+
...opts.pathPrefix === void 0 ? {} : { pathPrefix: opts.pathPrefix }
|
|
39217
|
+
}
|
|
39218
|
+
}
|
|
39219
|
+
);
|
|
39220
|
+
const item2 = page2.items[0];
|
|
39221
|
+
if (item2 === void 0) {
|
|
39222
|
+
source.done = page2.cursor === void 0;
|
|
39223
|
+
source.cursor = page2.cursor;
|
|
39224
|
+
return { statuses: [] };
|
|
39225
|
+
}
|
|
39226
|
+
source.pending = compactItem({ ...item2, source: { path: "" } });
|
|
39227
|
+
source.nextCursor = page2.cursor;
|
|
39228
|
+
return { statuses: [] };
|
|
39229
|
+
}
|
|
39230
|
+
const node = remoteNodes.get(source.path);
|
|
39231
|
+
if (node === void 0 || node.config?.kind !== "remote" || !check(input.ctx, node.path, "read").allow || !check(input.ctx, node.path, "call").allow) {
|
|
39232
|
+
throw invalidContinuation();
|
|
39233
|
+
}
|
|
39234
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
39235
|
+
const remainingMs = deadline - Date.now() - settings.perHopReturnReserveMs;
|
|
39236
|
+
if (remainingMs < settings.minChildWorkMs) {
|
|
39237
|
+
throw new TBError("unavailable", "federated search child has no remaining work budget", {
|
|
39238
|
+
retryable: false
|
|
39239
|
+
});
|
|
39240
|
+
}
|
|
39241
|
+
const headers = federatedHeaders(
|
|
39242
|
+
input.headers,
|
|
39243
|
+
remainingMs,
|
|
39244
|
+
source.budget,
|
|
39245
|
+
expiresAt - Date.now()
|
|
39246
|
+
);
|
|
39247
|
+
const childRequest = childSearchRequest(input, source, source.cursor);
|
|
39248
|
+
const response = await remoteResponse(
|
|
39249
|
+
deps,
|
|
39250
|
+
input,
|
|
39251
|
+
node,
|
|
39252
|
+
`${node.path}/~search`,
|
|
39253
|
+
"POST",
|
|
39254
|
+
headers,
|
|
39255
|
+
deadline,
|
|
39256
|
+
JSON.stringify(childRequest)
|
|
39257
|
+
);
|
|
39258
|
+
if (!response.ok) await throwChildSearchError(response);
|
|
39259
|
+
const parsed = toolSearchPageSchema.safeParse(await response.json().catch(() => null));
|
|
39260
|
+
if (!parsed.success) {
|
|
39261
|
+
throw new TBError("unavailable", "remote search returned invalid JSON", { retryable: false });
|
|
39262
|
+
}
|
|
39263
|
+
if (parsed.data.items.length > 1) {
|
|
39264
|
+
throw new TBError("unavailable", "remote search exceeded the requested source batch size", {
|
|
39265
|
+
retryable: false
|
|
39266
|
+
});
|
|
39267
|
+
}
|
|
39268
|
+
if (!isFederatedSearchSessionHandle(parsed.data.snapshot)) {
|
|
39269
|
+
throw new TBError("unavailable", "remote search omitted its validation snapshot", {
|
|
39270
|
+
retryable: false
|
|
39271
|
+
});
|
|
39272
|
+
}
|
|
39273
|
+
source.validationCursor = parsed.data.snapshot;
|
|
39274
|
+
const page = projectChildPage(
|
|
39275
|
+
source.path,
|
|
39276
|
+
parsed.data,
|
|
39277
|
+
totalTermCount,
|
|
39278
|
+
input.ctx,
|
|
39279
|
+
input.request
|
|
39280
|
+
);
|
|
39281
|
+
const item = page.items[0];
|
|
39282
|
+
if (item === void 0) {
|
|
39283
|
+
source.done = page.cursor === void 0;
|
|
39284
|
+
source.cursor = page.cursor;
|
|
39285
|
+
if (!source.done) {
|
|
39286
|
+
if (skipBudget <= 1) {
|
|
39287
|
+
throw new TBError("unavailable", "federated search source work budget exhausted", {
|
|
39288
|
+
retryable: false
|
|
39289
|
+
});
|
|
39290
|
+
}
|
|
39291
|
+
return await fetchHead(
|
|
39292
|
+
deps,
|
|
39293
|
+
input,
|
|
39294
|
+
source,
|
|
39295
|
+
remoteNodes,
|
|
39296
|
+
deadline,
|
|
39297
|
+
expiresAt,
|
|
39298
|
+
totalTermCount,
|
|
39299
|
+
skipBudget - 1
|
|
39300
|
+
);
|
|
39301
|
+
}
|
|
39302
|
+
} else {
|
|
39303
|
+
source.pending = item;
|
|
39304
|
+
source.nextCursor = page.cursor;
|
|
39305
|
+
}
|
|
39306
|
+
return { statuses: page.sources?.filter((status) => status.status !== "ok") ?? [] };
|
|
39307
|
+
}
|
|
39308
|
+
async function validateSourceSnapshots(deps, input, sources, remoteNodes, deadline, expiresAt, maxConcurrency) {
|
|
39309
|
+
const remotes = sources.filter((source) => source.kind === "remote" && source.excluded !== true);
|
|
39310
|
+
await mapLimit(remotes, maxConcurrency, async (source) => {
|
|
39311
|
+
const node = remoteNodes.get(source.path);
|
|
39312
|
+
if (node === void 0 || source.validationCursor === void 0) throw invalidContinuation();
|
|
39313
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
39314
|
+
const remainingMs = deadline - Date.now() - settings.perHopReturnReserveMs;
|
|
39315
|
+
if (remainingMs < settings.minChildWorkMs) throw retryableContinuationError();
|
|
39316
|
+
const headers = federatedHeaders(
|
|
39317
|
+
input.headers,
|
|
39318
|
+
remainingMs,
|
|
39319
|
+
source.budget,
|
|
39320
|
+
expiresAt - Date.now()
|
|
39321
|
+
);
|
|
39322
|
+
headers.set(HEADER_VALIDATE_SNAPSHOT, "1");
|
|
39323
|
+
const response = await remoteResponse(
|
|
39324
|
+
deps,
|
|
39325
|
+
input,
|
|
39326
|
+
node,
|
|
39327
|
+
`${node.path}/~search`,
|
|
39328
|
+
"POST",
|
|
39329
|
+
headers,
|
|
39330
|
+
deadline,
|
|
39331
|
+
JSON.stringify(childSearchRequest(input, source, source.validationCursor))
|
|
39332
|
+
);
|
|
39333
|
+
if (!response.ok) await throwChildSearchError(response);
|
|
39334
|
+
const parsed = toolSearchPageSchema.safeParse(await response.json().catch(() => null));
|
|
39335
|
+
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();
|
|
39336
|
+
});
|
|
39337
|
+
}
|
|
39338
|
+
function consume(source) {
|
|
39339
|
+
const item = source.pending;
|
|
39340
|
+
if (item === void 0) throw new Error("federated source has no pending item");
|
|
39341
|
+
source.pending = void 0;
|
|
39342
|
+
source.cursor = source.nextCursor;
|
|
39343
|
+
source.nextCursor = void 0;
|
|
39344
|
+
source.done = source.cursor === void 0;
|
|
39345
|
+
return item;
|
|
39346
|
+
}
|
|
39347
|
+
async function hydrateLocal(input, item) {
|
|
39348
|
+
if (!check(input.ctx, item.path, "read").allow || !check(input.ctx, item.path, "call").allow) {
|
|
39349
|
+
throw invalidContinuation();
|
|
39350
|
+
}
|
|
39351
|
+
const registry = new NodeRegistryStore(input.state);
|
|
39352
|
+
const node = await registry.get(item.path).catch(() => null);
|
|
39353
|
+
if (node === null || node.kind !== "mcp" && node.kind !== "http" && node.kind !== "tool" || node.config?.kind !== node.kind) {
|
|
39354
|
+
throw invalidContinuation();
|
|
39355
|
+
}
|
|
39356
|
+
const raw = (await canonicalSearchTools(input.state, [node])).get(node.path);
|
|
39357
|
+
if (raw === void 0) throw invalidContinuation();
|
|
39358
|
+
const matches = virtualizeTools(node.virtualize, raw).exposed.filter((tool2) => tool2.name === item.tool.name);
|
|
39359
|
+
if (matches.length !== 1) throw invalidContinuation();
|
|
39360
|
+
const tool = matches[0];
|
|
39361
|
+
if (tool === void 0) throw invalidContinuation();
|
|
39362
|
+
return { ...item, tool };
|
|
39363
|
+
}
|
|
39364
|
+
async function hydrateRemote(deps, input, item, source, node, deadline) {
|
|
39365
|
+
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) {
|
|
39366
|
+
throw invalidContinuation();
|
|
39367
|
+
}
|
|
39368
|
+
const response = await remoteResponse(
|
|
39369
|
+
deps,
|
|
39370
|
+
input,
|
|
39371
|
+
node,
|
|
39372
|
+
`${item.path}/${item.tool.name}/~help`,
|
|
39373
|
+
"GET",
|
|
39374
|
+
(() => {
|
|
39375
|
+
const headers = new Headers({ accept: "application/json" });
|
|
39376
|
+
const via = input.headers.get(HEADER_VIA);
|
|
39377
|
+
if (via !== null) headers.set(HEADER_VIA, via);
|
|
39378
|
+
headers.set(
|
|
39379
|
+
HEADER_REMAINING_MS,
|
|
39380
|
+
String(Math.max(1, Math.floor(deadline - Date.now())))
|
|
39381
|
+
);
|
|
39382
|
+
return headers;
|
|
39383
|
+
})(),
|
|
39384
|
+
deadline
|
|
39385
|
+
);
|
|
39386
|
+
if (!response.ok) {
|
|
39387
|
+
if (response.status >= 400 && response.status < 500) throw invalidContinuation();
|
|
39388
|
+
throw retryableContinuationError();
|
|
39389
|
+
}
|
|
39390
|
+
const parsed = helpJsonSchema.safeParse(await response.json().catch(() => null));
|
|
39391
|
+
if (!parsed.success) throw invalidContinuation();
|
|
39392
|
+
const projected = new RemotePathProjector(source.path).projectHelp(parsed.data, `${item.path}/${item.tool.name}`);
|
|
39393
|
+
const commands = projected.cmds.filter((cmd) => cmd.name === item.tool.name && cmd.path === `/${item.path}/${item.tool.name}` && cmd.scope === "call");
|
|
39394
|
+
if (commands.length !== 1) throw invalidContinuation();
|
|
39395
|
+
const command = commands[0];
|
|
39396
|
+
if (command === void 0) throw invalidContinuation();
|
|
39397
|
+
const tool = {
|
|
39398
|
+
name: command.name,
|
|
39399
|
+
...command.h === void 0 ? {} : { description: command.h },
|
|
39400
|
+
...command.effect === void 0 ? {} : { effect: command.effect },
|
|
39401
|
+
...command.confirm === void 0 ? {} : { confirm: command.confirm },
|
|
39402
|
+
...command.inputSchema === void 0 ? {} : { inputSchema: command.inputSchema },
|
|
39403
|
+
...command.outputSchema === void 0 ? {} : { outputSchema: command.outputSchema }
|
|
39404
|
+
};
|
|
39405
|
+
return { ...item, tool };
|
|
39406
|
+
}
|
|
39407
|
+
async function computePage(deps, input, sources, remoteNodes, deadline, expiresAt, totalTermCount, initial, initialExcluded, maxConcurrency) {
|
|
39408
|
+
let excluded = [...initialExcluded];
|
|
39409
|
+
if (!initial) {
|
|
39410
|
+
await validateSourceSnapshots(
|
|
39411
|
+
deps,
|
|
39412
|
+
input,
|
|
39413
|
+
sources,
|
|
39414
|
+
remoteNodes,
|
|
39415
|
+
deadline,
|
|
39416
|
+
expiresAt,
|
|
39417
|
+
maxConcurrency
|
|
39418
|
+
);
|
|
39419
|
+
}
|
|
39420
|
+
const active = sources.filter((source) => !source.done);
|
|
39421
|
+
const capabilityFailures = await mapLimit(
|
|
39422
|
+
initial ? active.filter((source) => source.kind === "remote") : [],
|
|
39423
|
+
maxConcurrency,
|
|
39424
|
+
async (source) => {
|
|
39425
|
+
const node = remoteNodes.get(source.path);
|
|
39426
|
+
if (node === void 0) return { source, status: "invalid_response" };
|
|
39427
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
39428
|
+
const remainingMs = deadline - Date.now() - settings.perHopReturnReserveMs;
|
|
39429
|
+
if (remainingMs < settings.minChildWorkMs) {
|
|
39430
|
+
return { source, status: "budget_exhausted" };
|
|
39431
|
+
}
|
|
39432
|
+
const headers = federatedHeaders(
|
|
39433
|
+
input.headers,
|
|
39434
|
+
remainingMs,
|
|
39435
|
+
source.budget,
|
|
39436
|
+
expiresAt - Date.now()
|
|
39437
|
+
);
|
|
39438
|
+
try {
|
|
39439
|
+
await assertFederatedCapability(deps, input, node, headers, deadline);
|
|
39440
|
+
return null;
|
|
39441
|
+
} catch (error) {
|
|
39442
|
+
const status = error instanceof TBError && error.message.includes("unsupported") ? "unsupported" : controlledStatus(error);
|
|
39443
|
+
return { error, source, status };
|
|
39444
|
+
}
|
|
39445
|
+
}
|
|
39446
|
+
);
|
|
39447
|
+
for (const failure of capabilityFailures) {
|
|
39448
|
+
if (failure === null) continue;
|
|
39449
|
+
if (!initial) {
|
|
39450
|
+
if (failure.error instanceof TBError && failure.error.code === "invalid_argument") {
|
|
39451
|
+
throw failure.error;
|
|
39452
|
+
}
|
|
39453
|
+
throw failure.status === "unsupported" ? invalidContinuation() : retryableContinuationError();
|
|
39454
|
+
}
|
|
39455
|
+
failure.source.done = true;
|
|
39456
|
+
failure.source.excluded = true;
|
|
39457
|
+
excluded = mergeStatuses(excluded, [{ path: failure.source.path, status: failure.status }]);
|
|
39458
|
+
}
|
|
39459
|
+
const storedPending = sources.filter((source) => source.pending !== void 0);
|
|
39460
|
+
const refreshedStatuses = await mapLimit(storedPending, maxConcurrency, async (source) => {
|
|
39461
|
+
try {
|
|
39462
|
+
const pending = source.pending;
|
|
39463
|
+
if (pending === void 0) throw invalidContinuation();
|
|
39464
|
+
const hydrated = source.kind === "local" ? await hydrateLocal(input, pending) : await (async () => {
|
|
39465
|
+
const node = remoteNodes.get(source.path);
|
|
39466
|
+
if (node === void 0) throw invalidContinuation();
|
|
39467
|
+
return await hydrateRemote(deps, input, pending, source, node, deadline);
|
|
39468
|
+
})();
|
|
39469
|
+
if (!sameJson(compactItem(hydrated), pending)) throw invalidContinuation();
|
|
39470
|
+
return [];
|
|
39471
|
+
} catch (error) {
|
|
39472
|
+
throw continuationSourceError(error);
|
|
39473
|
+
}
|
|
39474
|
+
});
|
|
39475
|
+
excluded = mergeStatuses(excluded, ...refreshedStatuses);
|
|
39476
|
+
let successfulSources = storedPending.length;
|
|
39477
|
+
const headResults = await mapLimit(
|
|
39478
|
+
sources.filter((source) => !source.done && source.pending === void 0),
|
|
39479
|
+
maxConcurrency,
|
|
39480
|
+
async (source) => {
|
|
39481
|
+
try {
|
|
39482
|
+
const result = await fetchHead(
|
|
39483
|
+
deps,
|
|
39484
|
+
input,
|
|
39485
|
+
source,
|
|
39486
|
+
remoteNodes,
|
|
39487
|
+
deadline,
|
|
39488
|
+
expiresAt,
|
|
39489
|
+
totalTermCount
|
|
39490
|
+
);
|
|
39491
|
+
return { ...result, success: true };
|
|
39492
|
+
} catch (error) {
|
|
39493
|
+
if (!initial) throw continuationSourceError(error);
|
|
39494
|
+
source.done = true;
|
|
39495
|
+
source.excluded = true;
|
|
39496
|
+
return {
|
|
39497
|
+
statuses: [{ path: source.path, status: controlledStatus(error) }],
|
|
39498
|
+
success: false
|
|
39499
|
+
};
|
|
39500
|
+
}
|
|
39501
|
+
}
|
|
39502
|
+
);
|
|
39503
|
+
successfulSources += headResults.filter((result) => result.success).length;
|
|
39504
|
+
excluded = mergeStatuses(excluded, ...headResults.map((result) => result.statuses));
|
|
39505
|
+
const items = [];
|
|
39506
|
+
let selectedCoverage;
|
|
39507
|
+
const limit = input.request.opts?.limit ?? 10;
|
|
39508
|
+
while (items.length < limit) {
|
|
39509
|
+
const heads = sources.filter((source2) => source2.pending !== void 0);
|
|
39510
|
+
if (heads.length === 0) break;
|
|
39511
|
+
const highest = Math.max(...heads.map((source2) => source2.pending?.relevance.coverage ?? -1));
|
|
39512
|
+
selectedCoverage ??= highest;
|
|
39513
|
+
if ((input.request.opts?.matching ?? "best") === "best" && highest !== selectedCoverage) break;
|
|
39514
|
+
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));
|
|
39515
|
+
const source = bucket[0];
|
|
39516
|
+
if (source === void 0) break;
|
|
39517
|
+
items.push(consume(source));
|
|
39518
|
+
if (source.balanceCoverage !== highest) {
|
|
39519
|
+
source.balanceCoverage = highest;
|
|
39520
|
+
source.balanceCount = 0;
|
|
39521
|
+
}
|
|
39522
|
+
source.balanceCount++;
|
|
39523
|
+
try {
|
|
39524
|
+
const result = await fetchHead(
|
|
39525
|
+
deps,
|
|
39526
|
+
input,
|
|
39527
|
+
source,
|
|
39528
|
+
remoteNodes,
|
|
39529
|
+
deadline,
|
|
39530
|
+
expiresAt,
|
|
39531
|
+
totalTermCount
|
|
39532
|
+
);
|
|
39533
|
+
excluded = mergeStatuses(excluded, result.statuses);
|
|
39534
|
+
} catch (error) {
|
|
39535
|
+
if (!initial) throw continuationSourceError(error);
|
|
39536
|
+
source.done = true;
|
|
39537
|
+
source.excluded = true;
|
|
39538
|
+
excluded = mergeStatuses(excluded, [{ path: source.path, status: controlledStatus(error) }]);
|
|
39539
|
+
}
|
|
39540
|
+
}
|
|
39541
|
+
const partial = excluded.length > 0;
|
|
39542
|
+
const page = {
|
|
39543
|
+
items,
|
|
39544
|
+
...partial ? { partial: true, sources: excluded } : {}
|
|
39545
|
+
};
|
|
39546
|
+
return { excluded, page, sources, successfulSources };
|
|
39547
|
+
}
|
|
39548
|
+
function visibleSourceStatuses(ctx, page) {
|
|
39549
|
+
const rawSources = page.sources;
|
|
39550
|
+
const rest = { ...page };
|
|
39551
|
+
delete rest.partial;
|
|
39552
|
+
delete rest.sources;
|
|
39553
|
+
const sources = rawSources?.filter((source) => check(ctx, source.path, "read").allow && check(ctx, source.path, "call").allow);
|
|
39554
|
+
const partial = sources?.some((source) => source.status !== "ok") === true;
|
|
39555
|
+
return {
|
|
39556
|
+
...rest,
|
|
39557
|
+
...partial ? { partial: true } : {},
|
|
39558
|
+
...sources === void 0 || sources.length === 0 ? {} : { sources }
|
|
39559
|
+
};
|
|
39560
|
+
}
|
|
39561
|
+
async function hydratePage(deps, input, page, sources, remoteNodes, deadline, forceRevalidate = false) {
|
|
39562
|
+
const visiblePage = visibleSourceStatuses(input.ctx, page);
|
|
39563
|
+
const detail = input.request.opts?.detail ?? "compact";
|
|
39564
|
+
if (detail !== "full" && !forceRevalidate) return visiblePage;
|
|
39565
|
+
const constraints = requestConstraints(input.request);
|
|
39566
|
+
const output = [];
|
|
39567
|
+
const encoder = new TextEncoder();
|
|
39568
|
+
let outputBytes = encoder.encode(JSON.stringify({ ...visiblePage, items: [] })).length;
|
|
39569
|
+
const concurrency = federatedSearchSettings(deps.remote).maxConcurrency;
|
|
39570
|
+
for (let offset = 0; offset < visiblePage.items.length; offset += concurrency) {
|
|
39571
|
+
const batch = visiblePage.items.slice(offset, offset + concurrency);
|
|
39572
|
+
const hydrated = await Promise.all(batch.map(async (item) => {
|
|
39573
|
+
const direct = sources.find((source) => source.kind === "remote" && isPrefix(source.path, item.path));
|
|
39574
|
+
const full = direct === void 0 ? await hydrateLocal(input, item) : await (async () => {
|
|
39575
|
+
const node = remoteNodes.get(direct.path);
|
|
39576
|
+
if (node === void 0) throw invalidContinuation();
|
|
39577
|
+
return await hydrateRemote(deps, input, item, direct, node, deadline);
|
|
39578
|
+
})();
|
|
39579
|
+
if (!sameJson(compactItem(full), compactItem(item))) throw invalidContinuation();
|
|
39580
|
+
assertHardResultConstraints(full, constraints);
|
|
39581
|
+
return full;
|
|
39582
|
+
}));
|
|
39583
|
+
if (detail !== "full") continue;
|
|
39584
|
+
for (const item of hydrated) {
|
|
39585
|
+
outputBytes += encoder.encode(JSON.stringify(item)).length;
|
|
39586
|
+
if (outputBytes > TOOL_SEARCH_PAGE_BYTES) {
|
|
39587
|
+
throw new TBError("rate_limited", "federated search page exceeds response byte budget", {
|
|
39588
|
+
retryable: false
|
|
39589
|
+
});
|
|
39590
|
+
}
|
|
39591
|
+
output.push(item);
|
|
39592
|
+
}
|
|
39593
|
+
}
|
|
39594
|
+
return detail === "full" ? { ...visiblePage, items: output } : visiblePage;
|
|
39595
|
+
}
|
|
39596
|
+
function pageWithCursor(page, cursor) {
|
|
39597
|
+
return cursor === void 0 ? page : { ...page, cursor };
|
|
39598
|
+
}
|
|
39599
|
+
function assertPageBytes(page) {
|
|
39600
|
+
if (new TextEncoder().encode(JSON.stringify(page)).length > TOOL_SEARCH_PAGE_BYTES) {
|
|
39601
|
+
throw new TBError("rate_limited", "federated search page exceeds response byte budget", {
|
|
39602
|
+
retryable: false
|
|
39603
|
+
});
|
|
39604
|
+
}
|
|
39605
|
+
}
|
|
39606
|
+
async function executeFederatedSearch(deps, input) {
|
|
39607
|
+
if (deps.remote.instanceId === void 0 || input.state.compareAndSwap === void 0) {
|
|
39608
|
+
throw new TBError(
|
|
39609
|
+
"invalid_argument",
|
|
39610
|
+
"recursive search requires explicit TB_INSTANCE_ID and atomic StateStore CAS"
|
|
39611
|
+
);
|
|
39612
|
+
}
|
|
39613
|
+
if ((input.request.opts?.mode ?? "keyword") !== "keyword") {
|
|
39614
|
+
throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
|
|
39615
|
+
}
|
|
39616
|
+
const wireOpts = input.request.opts ?? {};
|
|
39617
|
+
const constraints = normalizeToolSearchOptions({
|
|
39618
|
+
...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
|
|
39619
|
+
matching: wireOpts.matching ?? "best",
|
|
39620
|
+
...wireOpts.minCoverage === void 0 ? {} : { minCoverage: wireOpts.minCoverage },
|
|
39621
|
+
...wireOpts.pathPrefix === void 0 ? {} : { pathPrefix: wireOpts.pathPrefix }
|
|
39622
|
+
});
|
|
39623
|
+
input = {
|
|
39624
|
+
...input,
|
|
39625
|
+
request: {
|
|
39626
|
+
query: input.request.query,
|
|
39627
|
+
opts: {
|
|
39628
|
+
...wireOpts,
|
|
39629
|
+
...constraints.effects === void 0 ? {} : { effects: constraints.effects },
|
|
39630
|
+
matching: constraints.matching,
|
|
39631
|
+
...constraints.minCoverage === void 0 ? {} : { minCoverage: constraints.minCoverage },
|
|
39632
|
+
...constraints.pathPrefix === void 0 ? {} : { pathPrefix: constraints.pathPrefix }
|
|
39633
|
+
}
|
|
39634
|
+
}
|
|
39635
|
+
};
|
|
39636
|
+
const viaError = checkVia(
|
|
39637
|
+
parseVia(input.headers.get(HEADER_VIA) ?? void 0),
|
|
39638
|
+
deps.remote.instanceId,
|
|
39639
|
+
deps.remote.maxHops
|
|
39640
|
+
);
|
|
39641
|
+
if (viaError !== null) throw viaError;
|
|
39642
|
+
const settings = federatedSearchSettings(deps.remote);
|
|
39643
|
+
await input.searchSync?.ensureReady();
|
|
39644
|
+
if (input.search.revision === void 0) {
|
|
39645
|
+
throw new TBError("invalid_argument", "recursive search requires a revisioned search index");
|
|
39646
|
+
}
|
|
39647
|
+
const localRevision = await input.search.revision();
|
|
39648
|
+
const deadlineMs = clampHeader(input.headers.get(HEADER_REMAINING_MS), settings.totalDeadlineMs);
|
|
39649
|
+
const sessionTtlMs = clampHeader(input.headers.get(HEADER_SESSION_TTL_MS), settings.sessionTtlMs);
|
|
39650
|
+
const sourceBudget = clampHeader(input.headers.get(HEADER_SOURCE_BUDGET), settings.maxSources);
|
|
39651
|
+
const deadline = Date.now() + deadlineMs;
|
|
39652
|
+
const expiresAtMs = Date.now() + sessionTtlMs;
|
|
39653
|
+
const limit = input.request.opts?.limit ?? 10;
|
|
39654
|
+
const requestDigest = await sha256Hex(canonicalJson({
|
|
39655
|
+
authorization: input.ctx.scopes,
|
|
39656
|
+
request: requestForDigest(input.request, limit)
|
|
39657
|
+
}));
|
|
39658
|
+
const sessionStore = new FederatedSearchSessionStore(input.state);
|
|
39659
|
+
const plan = prepareToolSearchQuery(input.request.query);
|
|
39660
|
+
const registry = new NodeRegistryStore(input.state);
|
|
39661
|
+
const snapshot = await registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
|
|
39662
|
+
const allDirect = directRemoteNodes(snapshot.items);
|
|
39663
|
+
const eligibleRemotes = allDirect.filter((node) => {
|
|
39664
|
+
const relation = childPrefixFor(node.path, input.request.opts?.pathPrefix);
|
|
39665
|
+
return relation.eligible && check(input.ctx, node.path, "read").allow && check(input.ctx, node.path, "call").allow;
|
|
39666
|
+
});
|
|
39667
|
+
const includeLocal = localEligible(allDirect, input.request.opts?.pathPrefix);
|
|
39668
|
+
const configuredRemotes = eligibleRemotes.filter((node) => node.config?.kind === "remote" && node.config.skRef !== void 0);
|
|
39669
|
+
const secretAvailability = await mapLimit(
|
|
39670
|
+
configuredRemotes,
|
|
39671
|
+
settings.maxConcurrency,
|
|
39672
|
+
async (node) => {
|
|
39673
|
+
if (node.config?.kind !== "remote" || node.config.skRef === void 0) return false;
|
|
39674
|
+
try {
|
|
39675
|
+
return await deps.secrets.resolve(node.config.skRef) !== void 0;
|
|
39676
|
+
} catch {
|
|
39677
|
+
return false;
|
|
39678
|
+
}
|
|
39679
|
+
}
|
|
39680
|
+
);
|
|
39681
|
+
const participatingRemotes = configuredRemotes.filter((_node, index) => secretAvailability[index] === true);
|
|
39682
|
+
const unavailableRemotes = configuredRemotes.filter((_node, index) => secretAvailability[index] !== true);
|
|
39683
|
+
const transportSettings = await resolveRemoteSettings(input.state, deps.remote);
|
|
39684
|
+
const topologyNodes = snapshot.items.filter((node) => check(input.ctx, node.path, "read").allow && check(input.ctx, node.path, "call").allow);
|
|
39685
|
+
const digest = await topologyDigest(topologyNodes, includeLocal, localRevision, {
|
|
39686
|
+
allowInsecure: transportSettings.allowInsecure,
|
|
39687
|
+
allowlist: [...transportSettings.allowlist].sort(),
|
|
39688
|
+
availableRemotes: participatingRemotes.map((node) => node.path),
|
|
39689
|
+
instanceId: transportSettings.instanceId ?? null,
|
|
39690
|
+
maxHops: transportSettings.maxHops
|
|
39691
|
+
});
|
|
39692
|
+
const remoteNodes = new Map(participatingRemotes.map((node) => [node.path, node]));
|
|
39693
|
+
const cursor = input.request.opts?.cursor;
|
|
39694
|
+
if (cursor !== void 0) {
|
|
39695
|
+
const current = await sessionStore.read(cursor, { actorKeyId: input.ctx.keyId, requestDigest });
|
|
39696
|
+
if (current.topologyDigest !== digest || current.rankingVersion !== TOOL_SEARCH_RANKING_VERSION) {
|
|
39697
|
+
throw invalidContinuation();
|
|
39698
|
+
}
|
|
39699
|
+
if (input.headers.get(HEADER_VALIDATE_SNAPSHOT) === "1") {
|
|
39700
|
+
await validateSourceSnapshots(
|
|
39701
|
+
deps,
|
|
39702
|
+
input,
|
|
39703
|
+
current.sourceContinuations,
|
|
39704
|
+
remoteNodes,
|
|
39705
|
+
deadline,
|
|
39706
|
+
Date.parse(current.expiresAt),
|
|
39707
|
+
settings.maxConcurrency
|
|
39708
|
+
);
|
|
39709
|
+
return { items: [] };
|
|
39710
|
+
}
|
|
39711
|
+
if (current.nextHandle === null) throw invalidContinuation();
|
|
39712
|
+
let next;
|
|
39713
|
+
try {
|
|
39714
|
+
next = await sessionStore.read(current.nextHandle, {
|
|
39715
|
+
actorKeyId: input.ctx.keyId,
|
|
39716
|
+
requestDigest
|
|
39717
|
+
});
|
|
39718
|
+
} catch (error) {
|
|
39719
|
+
if (!(error instanceof TBError) || error.code !== "invalid_argument") throw error;
|
|
39720
|
+
}
|
|
39721
|
+
if (next !== void 0) assertNextRecord(current, next, digest);
|
|
39722
|
+
const cachedNext = next !== void 0;
|
|
39723
|
+
if (next === void 0) {
|
|
39724
|
+
const state2 = await computePage(
|
|
39725
|
+
deps,
|
|
39726
|
+
input,
|
|
39727
|
+
structuredClone(current.sourceContinuations),
|
|
39728
|
+
remoteNodes,
|
|
39729
|
+
deadline,
|
|
39730
|
+
Date.parse(current.expiresAt),
|
|
39731
|
+
plan.totalTermCount,
|
|
39732
|
+
false,
|
|
39733
|
+
current.excludedStatuses,
|
|
39734
|
+
settings.maxConcurrency
|
|
39735
|
+
);
|
|
39736
|
+
const hasMore2 = responseHasMore(state2.sources);
|
|
39737
|
+
const nextNextHandle = hasMore2 ? sessionStore.issueHandle() : null;
|
|
39738
|
+
next = await sessionStore.create(current.nextHandle, {
|
|
39739
|
+
actorKeyId: input.ctx.keyId,
|
|
39740
|
+
excludedStatuses: state2.excluded,
|
|
39741
|
+
expiresAt: current.expiresAt,
|
|
39742
|
+
federationPolicy: current.federationPolicy,
|
|
39743
|
+
generation: current.generation + 1,
|
|
39744
|
+
nextHandle: nextNextHandle,
|
|
39745
|
+
page: state2.page,
|
|
39746
|
+
rankingVersion: TOOL_SEARCH_RANKING_VERSION,
|
|
39747
|
+
requestDigest,
|
|
39748
|
+
sessionId: current.sessionId,
|
|
39749
|
+
sourceContinuations: serializableSources(state2.sources),
|
|
39750
|
+
topologyDigest: digest
|
|
39751
|
+
});
|
|
39752
|
+
assertNextRecord(current, next, digest);
|
|
39753
|
+
assertCachedPage(state2, next);
|
|
39754
|
+
}
|
|
39755
|
+
if (cachedNext) {
|
|
39756
|
+
await validateSourceSnapshots(
|
|
39757
|
+
deps,
|
|
39758
|
+
input,
|
|
39759
|
+
next.sourceContinuations,
|
|
39760
|
+
remoteNodes,
|
|
39761
|
+
deadline,
|
|
39762
|
+
Date.parse(next.expiresAt),
|
|
39763
|
+
settings.maxConcurrency
|
|
39764
|
+
);
|
|
39765
|
+
}
|
|
39766
|
+
const wantsSnapshot2 = input.headers.get(HEADER_WANT_SNAPSHOT) === "1";
|
|
39767
|
+
const responsePage2 = {
|
|
39768
|
+
...pageWithCursor(
|
|
39769
|
+
next.page,
|
|
39770
|
+
next.nextHandle === null ? void 0 : current.nextHandle
|
|
39771
|
+
),
|
|
39772
|
+
...wantsSnapshot2 ? { snapshot: current.nextHandle } : {}
|
|
39773
|
+
};
|
|
39774
|
+
const response2 = await hydratePage(
|
|
39775
|
+
deps,
|
|
39776
|
+
input,
|
|
39777
|
+
responsePage2,
|
|
39778
|
+
next.sourceContinuations,
|
|
39779
|
+
remoteNodes,
|
|
39780
|
+
deadline,
|
|
39781
|
+
cachedNext
|
|
39782
|
+
);
|
|
39783
|
+
assertPageBytes(response2);
|
|
39784
|
+
return response2;
|
|
39785
|
+
}
|
|
39786
|
+
const initialExcluded = eligibleRemotes.filter((node) => node.config?.kind === "remote" && node.config.skRef === void 0).map((node) => ({ path: node.path, status: "unsupported" }));
|
|
39787
|
+
initialExcluded.push(...unavailableRemotes.map((node) => ({
|
|
39788
|
+
path: node.path,
|
|
39789
|
+
status: "unavailable"
|
|
39790
|
+
})));
|
|
39791
|
+
const availableRemoteSlots = Math.max(0, sourceBudget - (includeLocal ? 1 : 0));
|
|
39792
|
+
const selectedRemotes = participatingRemotes.slice(0, availableRemoteSlots);
|
|
39793
|
+
for (const node of participatingRemotes.slice(availableRemoteSlots)) {
|
|
39794
|
+
initialExcluded.push({ path: node.path, status: "budget_exhausted" });
|
|
39795
|
+
}
|
|
39796
|
+
if (snapshot.truncated) initialExcluded.push({ path: "", status: "budget_exhausted" });
|
|
39797
|
+
const allocations = selectedRemotes.map((_node, index) => {
|
|
39798
|
+
const base = selectedRemotes.length === 0 ? 0 : Math.floor(availableRemoteSlots / selectedRemotes.length);
|
|
39799
|
+
const extra = index < availableRemoteSlots % Math.max(1, selectedRemotes.length) ? 1 : 0;
|
|
39800
|
+
return Math.max(1, base + extra);
|
|
39801
|
+
});
|
|
39802
|
+
const sources = [
|
|
39803
|
+
...includeLocal ? [{ balanceCount: 0, budget: 1, done: false, kind: "local", path: "" }] : [],
|
|
39804
|
+
...selectedRemotes.map((node, index) => ({
|
|
39805
|
+
balanceCount: 0,
|
|
39806
|
+
budget: allocations[index] ?? 1,
|
|
39807
|
+
done: false,
|
|
39808
|
+
kind: "remote",
|
|
39809
|
+
path: node.path
|
|
39810
|
+
}))
|
|
39811
|
+
];
|
|
39812
|
+
const state = await computePage(
|
|
39813
|
+
deps,
|
|
39814
|
+
input,
|
|
39815
|
+
sources,
|
|
39816
|
+
remoteNodes,
|
|
39817
|
+
deadline,
|
|
39818
|
+
expiresAtMs,
|
|
39819
|
+
plan.totalTermCount,
|
|
39820
|
+
true,
|
|
39821
|
+
initialExcluded,
|
|
39822
|
+
settings.maxConcurrency
|
|
39823
|
+
);
|
|
39824
|
+
const candidateSourceCount = (includeLocal ? 1 : 0) + eligibleRemotes.length;
|
|
39825
|
+
if (candidateSourceCount > 0 && state.successfulSources === 0 && state.page.items.length === 0) {
|
|
39826
|
+
throw new TBError("unavailable", "all federated search sources failed", { retryable: true });
|
|
39827
|
+
}
|
|
39828
|
+
const hasMore = responseHasMore(state.sources);
|
|
39829
|
+
const wantsSnapshot = input.headers.get(HEADER_WANT_SNAPSHOT) === "1";
|
|
39830
|
+
const needsSession = hasMore || wantsSnapshot;
|
|
39831
|
+
const sessionHandle = needsSession ? sessionStore.issueHandle() : void 0;
|
|
39832
|
+
const responseCursor = hasMore ? sessionHandle : void 0;
|
|
39833
|
+
const nextHandle = hasMore ? sessionStore.issueHandle() : null;
|
|
39834
|
+
const responsePage = {
|
|
39835
|
+
...pageWithCursor(state.page, responseCursor),
|
|
39836
|
+
...wantsSnapshot && sessionHandle !== void 0 ? { snapshot: sessionHandle } : {}
|
|
39837
|
+
};
|
|
39838
|
+
const response = await hydratePage(
|
|
39839
|
+
deps,
|
|
39840
|
+
input,
|
|
39841
|
+
responsePage,
|
|
39842
|
+
state.sources,
|
|
39843
|
+
remoteNodes,
|
|
39844
|
+
deadline
|
|
39845
|
+
);
|
|
39846
|
+
assertPageBytes(response);
|
|
39847
|
+
if (sessionHandle !== void 0) {
|
|
39848
|
+
await sessionStore.create(sessionHandle, {
|
|
39849
|
+
actorKeyId: input.ctx.keyId,
|
|
39850
|
+
excludedStatuses: state.excluded,
|
|
39851
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
39852
|
+
federationPolicy: { limit, matching: input.request.opts?.matching ?? "best" },
|
|
39853
|
+
generation: 0,
|
|
39854
|
+
nextHandle,
|
|
39855
|
+
page: state.page,
|
|
39856
|
+
rankingVersion: TOOL_SEARCH_RANKING_VERSION,
|
|
39857
|
+
requestDigest,
|
|
39858
|
+
sourceContinuations: serializableSources(state.sources),
|
|
39859
|
+
topologyDigest: digest
|
|
39860
|
+
});
|
|
39861
|
+
}
|
|
39862
|
+
return response;
|
|
39863
|
+
}
|
|
39864
|
+
|
|
37598
39865
|
// ../app/src/routes/search.ts
|
|
39866
|
+
function requestWithNormalizedLimit(request) {
|
|
39867
|
+
return {
|
|
39868
|
+
...request,
|
|
39869
|
+
opts: {
|
|
39870
|
+
...request.opts ?? {},
|
|
39871
|
+
limit: normalizeToolSearchLimit(request.opts?.limit)
|
|
39872
|
+
}
|
|
39873
|
+
};
|
|
39874
|
+
}
|
|
37599
39875
|
function registerSearchRoute(app, env) {
|
|
37600
39876
|
const { deps, globalSearchCapabilities, searchSync } = env;
|
|
37601
39877
|
app.post(
|
|
@@ -37610,8 +39886,7 @@ function registerSearchRoute(app, env) {
|
|
|
37610
39886
|
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
37611
39887
|
throw new TBError("invalid_argument", "body must be a JSON object");
|
|
37612
39888
|
}
|
|
37613
|
-
|
|
37614
|
-
if (bodyKeys.some((key) => key !== "query" && key !== "opts")) {
|
|
39889
|
+
if (Object.keys(body).some((key) => key !== "query" && key !== "opts")) {
|
|
37615
39890
|
throw new TBError("invalid_argument", "body only accepts query and opts");
|
|
37616
39891
|
}
|
|
37617
39892
|
if (typeof body.query !== "string" || body.query.trim().length === 0) {
|
|
@@ -37622,105 +39897,71 @@ function registerSearchRoute(app, env) {
|
|
|
37622
39897
|
throw new TBError("invalid_argument", "opts must be a JSON object");
|
|
37623
39898
|
}
|
|
37624
39899
|
const opts = rawOpts ?? {};
|
|
37625
|
-
|
|
37626
|
-
|
|
39900
|
+
const optionKeys = /* @__PURE__ */ new Set([
|
|
39901
|
+
"cursor",
|
|
39902
|
+
"detail",
|
|
39903
|
+
"effects",
|
|
39904
|
+
"federation",
|
|
39905
|
+
"limit",
|
|
39906
|
+
"matching",
|
|
39907
|
+
"minCoverage",
|
|
39908
|
+
"mode",
|
|
39909
|
+
"pathPrefix"
|
|
39910
|
+
]);
|
|
39911
|
+
if (Object.keys(opts).some((key) => !optionKeys.has(key))) {
|
|
39912
|
+
throw new TBError("invalid_argument", "opts contains an unknown search option");
|
|
37627
39913
|
}
|
|
37628
|
-
|
|
37629
|
-
|
|
37630
|
-
throw new TBError("invalid_argument", "opts.mode must be 'keyword' or 'semantic'");
|
|
39914
|
+
if (opts.cursor !== void 0 && typeof opts.cursor !== "string") {
|
|
39915
|
+
throw new TBError("invalid_argument", "opts.cursor must be a string");
|
|
37631
39916
|
}
|
|
39917
|
+
const parsed = toolSearchRequestSchema.safeParse({
|
|
39918
|
+
query: body.query,
|
|
39919
|
+
...rawOpts === void 0 ? {} : { opts }
|
|
39920
|
+
});
|
|
39921
|
+
if (!parsed.success) {
|
|
39922
|
+
throw new TBError(
|
|
39923
|
+
"invalid_argument",
|
|
39924
|
+
parsed.error.issues[0]?.message ?? "invalid search request"
|
|
39925
|
+
);
|
|
39926
|
+
}
|
|
39927
|
+
const request = requestWithNormalizedLimit(parsed.data);
|
|
39928
|
+
const mode = request.opts?.mode ?? "keyword";
|
|
37632
39929
|
if (mode === "semantic" && !capabilities.includes("search:semantic")) {
|
|
37633
39930
|
throw new TBError(
|
|
37634
39931
|
"invalid_argument",
|
|
37635
39932
|
"search mode 'semantic' requires capability 'search:semantic'"
|
|
37636
39933
|
);
|
|
37637
39934
|
}
|
|
37638
|
-
const
|
|
37639
|
-
|
|
37640
|
-
|
|
37641
|
-
|
|
37642
|
-
|
|
37643
|
-
|
|
37644
|
-
|
|
37645
|
-
|
|
37646
|
-
const query = wireRequest.query;
|
|
37647
|
-
await searchSync?.ensureReady();
|
|
37648
|
-
const ctx = c.get("ctx");
|
|
37649
|
-
const registry = new NodeRegistryStore(c.get("store"));
|
|
37650
|
-
const selected = [];
|
|
37651
|
-
const workLimit = Math.min(
|
|
37652
|
-
TOOL_SEARCH_WORK_LIMIT,
|
|
37653
|
-
Math.max(TOOL_SEARCH_BATCH_LIMIT, limit * 2)
|
|
37654
|
-
);
|
|
37655
|
-
let scanCursor = opts.cursor;
|
|
37656
|
-
let responseCursor;
|
|
37657
|
-
let responseCandidate;
|
|
37658
|
-
let scanned = 0;
|
|
37659
|
-
let stopped = false;
|
|
37660
|
-
while (!stopped && selected.length < limit && scanned < workLimit) {
|
|
37661
|
-
const batchLimit = Math.min(TOOL_SEARCH_BATCH_LIMIT, workLimit - scanned);
|
|
37662
|
-
const page = await search.search(query, {
|
|
37663
|
-
mode,
|
|
37664
|
-
limit: batchLimit,
|
|
37665
|
-
...scanCursor === void 0 ? {} : { cursor: scanCursor }
|
|
37666
|
-
});
|
|
37667
|
-
if (page.items.length === 0) {
|
|
37668
|
-
responseCursor = page.cursor;
|
|
37669
|
-
break;
|
|
37670
|
-
}
|
|
37671
|
-
const nodes = await registry.getMany(page.items.map((candidate) => candidate.path));
|
|
37672
|
-
for (const [index, candidate] of page.items.entries()) {
|
|
37673
|
-
scanned++;
|
|
37674
|
-
if (!check(ctx, candidate.path, "read").allow || !check(ctx, candidate.path, "call").allow) {
|
|
37675
|
-
continue;
|
|
37676
|
-
}
|
|
37677
|
-
const node = nodes.get(candidate.path);
|
|
37678
|
-
if (node === void 0 || node.kind !== "mcp" && node.kind !== "http" && node.kind !== "tool" || node.config?.kind !== node.kind || virtualizeTools(node.virtualize, [{ name: candidate.name }]).exposed.length === 0) {
|
|
37679
|
-
continue;
|
|
37680
|
-
}
|
|
37681
|
-
selected.push({ candidate, node });
|
|
37682
|
-
if (selected.length === limit) {
|
|
37683
|
-
const hasUnscanned = index < page.items.length - 1 || page.cursor !== void 0;
|
|
37684
|
-
responseCandidate = hasUnscanned ? candidate : void 0;
|
|
37685
|
-
responseCursor = void 0;
|
|
37686
|
-
stopped = true;
|
|
37687
|
-
break;
|
|
37688
|
-
}
|
|
37689
|
-
}
|
|
37690
|
-
if (stopped) break;
|
|
37691
|
-
responseCursor = page.cursor;
|
|
37692
|
-
if (page.cursor === void 0) break;
|
|
37693
|
-
scanCursor = page.cursor;
|
|
37694
|
-
}
|
|
37695
|
-
const canonicalTools = await canonicalSearchTools(
|
|
37696
|
-
c.get("store"),
|
|
37697
|
-
selected.map((item) => item.node)
|
|
37698
|
-
);
|
|
37699
|
-
const items = [];
|
|
37700
|
-
let pageBytes = 0;
|
|
37701
|
-
for (const [index, selectedItem] of selected.entries()) {
|
|
37702
|
-
const raw = canonicalTools.get(selectedItem.candidate.path)?.find((tool2) => tool2.name === selectedItem.candidate.name);
|
|
37703
|
-
if (raw === void 0) continue;
|
|
37704
|
-
const tool = virtualizeTools(selectedItem.node.virtualize, [raw]).exposed[0];
|
|
37705
|
-
if (tool === void 0) continue;
|
|
37706
|
-
const item = { path: selectedItem.candidate.path, tool };
|
|
37707
|
-
const itemBytes = new TextEncoder().encode(JSON.stringify(item)).length;
|
|
37708
|
-
if (pageBytes + itemBytes > TOOL_SEARCH_PAGE_BYTES) {
|
|
37709
|
-
if (items.length === 0) {
|
|
37710
|
-
throw new TBError("internal", "\u5DE5\u5177\u641C\u7D22\u9875\u9762\u5B57\u8282\u9884\u7B97\u65E0\u6CD5\u5BB9\u7EB3\u9996\u4E2A\u7ED3\u679C");
|
|
37711
|
-
}
|
|
37712
|
-
responseCandidate = selected[index - 1]?.candidate;
|
|
37713
|
-
responseCursor = void 0;
|
|
37714
|
-
break;
|
|
37715
|
-
}
|
|
37716
|
-
items.push(item);
|
|
37717
|
-
pageBytes += itemBytes;
|
|
39935
|
+
const explicitFederation = request.opts?.federation;
|
|
39936
|
+
const cursor = request.opts?.cursor;
|
|
39937
|
+
const federation = explicitFederation ?? (cursor !== void 0 && !isFederatedSearchSessionHandle(cursor) ? "local" : capabilities.includes("search:federated") ? "recursive" : "local");
|
|
39938
|
+
if (federation === "recursive" && !capabilities.includes("search:federated")) {
|
|
39939
|
+
throw new TBError(
|
|
39940
|
+
"invalid_argument",
|
|
39941
|
+
"recursive search requires capability 'search:federated'"
|
|
39942
|
+
);
|
|
37718
39943
|
}
|
|
37719
|
-
if (
|
|
37720
|
-
|
|
39944
|
+
if (federation === "recursive" && mode === "semantic") {
|
|
39945
|
+
throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
|
|
37721
39946
|
}
|
|
37722
|
-
|
|
37723
|
-
|
|
39947
|
+
const normalizedRequest = {
|
|
39948
|
+
...request,
|
|
39949
|
+
opts: { ...request.opts ?? {}, federation }
|
|
39950
|
+
};
|
|
39951
|
+
const result = federation === "local" ? await executeLocalSearch({
|
|
39952
|
+
ctx: c.get("ctx"),
|
|
39953
|
+
search,
|
|
39954
|
+
...searchSync === void 0 ? {} : { searchSync },
|
|
39955
|
+
state: c.get("store")
|
|
39956
|
+
}, normalizedRequest) : await executeFederatedSearch(deps, {
|
|
39957
|
+
ctx: c.get("ctx"),
|
|
39958
|
+
headers: c.req.raw.headers,
|
|
39959
|
+
request: normalizedRequest,
|
|
39960
|
+
requestUrl: c.req.url,
|
|
39961
|
+
search,
|
|
39962
|
+
...searchSync === void 0 ? {} : { searchSync },
|
|
39963
|
+
state: c.get("store")
|
|
39964
|
+
});
|
|
37724
39965
|
return new Response(JSON.stringify(toolSearchPageSchema.parse(result)), {
|
|
37725
39966
|
headers: { "content-type": contentTypeFor("json") }
|
|
37726
39967
|
});
|
|
@@ -37866,6 +40107,7 @@ var mcpCommand = (nodePath, nodeDescription, command) => {
|
|
|
37866
40107
|
invokePath,
|
|
37867
40108
|
description: command.h ?? nodeDescription,
|
|
37868
40109
|
...command.inputSchema !== void 0 ? { inputSchema: command.inputSchema } : {},
|
|
40110
|
+
...command.delivery !== void 0 ? { delivery: command.delivery } : {},
|
|
37869
40111
|
...command.effect !== void 0 ? { effect: command.effect } : {},
|
|
37870
40112
|
...command.confirm === true ? { confirm: true } : {}
|
|
37871
40113
|
};
|
|
@@ -37880,6 +40122,7 @@ var toolSpecCommand = (node, tool, providerBacked = false) => {
|
|
|
37880
40122
|
...providerBacked ? { providerBacked: true } : {},
|
|
37881
40123
|
description: tool.description ?? node.description,
|
|
37882
40124
|
...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
|
|
40125
|
+
...tool.delivery !== void 0 ? { delivery: tool.delivery } : {},
|
|
37883
40126
|
...tool.effect !== void 0 ? { effect: tool.effect } : {},
|
|
37884
40127
|
...tool.confirm === true ? { confirm: true } : {}
|
|
37885
40128
|
};
|
|
@@ -37909,7 +40152,7 @@ function mcpBridgeFor(c, env, app) {
|
|
|
37909
40152
|
invokePath: "/~search",
|
|
37910
40153
|
mcpName: "tb_search",
|
|
37911
40154
|
operation: "search",
|
|
37912
|
-
description: "Search visible tools across the Tool Bridge tree.",
|
|
40155
|
+
description: "Search visible tools across the Tool Bridge tree (compact by default).",
|
|
37913
40156
|
effect: "read",
|
|
37914
40157
|
inputSchema: {
|
|
37915
40158
|
type: "object",
|
|
@@ -37918,7 +40161,20 @@ function mcpBridgeFor(c, env, app) {
|
|
|
37918
40161
|
query: { type: "string", minLength: 1 },
|
|
37919
40162
|
mode: { type: "string", enum: ["keyword", "semantic"] },
|
|
37920
40163
|
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
37921
|
-
cursor: { type: "string", minLength: 1 }
|
|
40164
|
+
cursor: { type: "string", minLength: 1 },
|
|
40165
|
+
detail: { type: "string", enum: ["compact", "full"], default: "compact" },
|
|
40166
|
+
effects: {
|
|
40167
|
+
type: "array",
|
|
40168
|
+
minItems: 1,
|
|
40169
|
+
items: {
|
|
40170
|
+
type: "string",
|
|
40171
|
+
enum: ["read", "write", "destructive", "unknown"]
|
|
40172
|
+
}
|
|
40173
|
+
},
|
|
40174
|
+
federation: { type: "string", enum: ["local", "recursive"] },
|
|
40175
|
+
matching: { type: "string", enum: ["best", "all"] },
|
|
40176
|
+
minCoverage: { type: "number", exclusiveMinimum: 0, maximum: 1 },
|
|
40177
|
+
pathPrefix: { type: "string", minLength: 1 }
|
|
37922
40178
|
},
|
|
37923
40179
|
required: ["query"]
|
|
37924
40180
|
}
|
|
@@ -38126,15 +40382,27 @@ function mcpBridgeFor(c, env, app) {
|
|
|
38126
40382
|
if (tool.operation === "search") {
|
|
38127
40383
|
headers2.set("accept", "application/json");
|
|
38128
40384
|
headers2.set("content-type", "application/json");
|
|
38129
|
-
const opts =
|
|
38130
|
-
|
|
38131
|
-
|
|
40385
|
+
const opts = {
|
|
40386
|
+
detail: args.detail ?? "compact",
|
|
40387
|
+
...Object.fromEntries(
|
|
40388
|
+
[
|
|
40389
|
+
"mode",
|
|
40390
|
+
"limit",
|
|
40391
|
+
"cursor",
|
|
40392
|
+
"effects",
|
|
40393
|
+
"federation",
|
|
40394
|
+
"matching",
|
|
40395
|
+
"minCoverage",
|
|
40396
|
+
"pathPrefix"
|
|
40397
|
+
].filter((key) => args[key] !== void 0).map((key) => [key, args[key]])
|
|
40398
|
+
)
|
|
40399
|
+
};
|
|
38132
40400
|
const response2 = await app.request(new Request(new URL("/~search", c.req.url), {
|
|
38133
40401
|
method: "POST",
|
|
38134
40402
|
headers: headers2,
|
|
38135
40403
|
body: JSON.stringify({
|
|
38136
40404
|
query: args.query,
|
|
38137
|
-
|
|
40405
|
+
opts
|
|
38138
40406
|
})
|
|
38139
40407
|
}));
|
|
38140
40408
|
return await resultFromResponse(response2);
|
|
@@ -38178,11 +40446,11 @@ function mcpBridgeFor(c, env, app) {
|
|
|
38178
40446
|
const provider = await providerFor(node, ctx, deps);
|
|
38179
40447
|
const raw = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString());
|
|
38180
40448
|
const upstreamName = resolveUpstreamTool(node.virtualize, raw, tool.toolName);
|
|
38181
|
-
const
|
|
40449
|
+
const result2 = await provider.call(upstreamName, args);
|
|
38182
40450
|
return {
|
|
38183
|
-
content:
|
|
38184
|
-
...
|
|
38185
|
-
...
|
|
40451
|
+
content: result2.contentBlocks ?? result2.content,
|
|
40452
|
+
...result2.isError === true ? { isError: true } : {},
|
|
40453
|
+
...result2.structuredContent !== void 0 ? { structuredContent: result2.structuredContent } : {}
|
|
38186
40454
|
};
|
|
38187
40455
|
}
|
|
38188
40456
|
const url2 = new URL(tool.invokePath, c.req.url);
|
|
@@ -38194,7 +40462,13 @@ function mcpBridgeFor(c, env, app) {
|
|
|
38194
40462
|
const response = await app.request(
|
|
38195
40463
|
new Request(url2, { method: "POST", headers, body: JSON.stringify(args) })
|
|
38196
40464
|
);
|
|
38197
|
-
|
|
40465
|
+
const result = await resultFromResponse(response);
|
|
40466
|
+
if (args["~delivery"] !== void 0 && response.ok) {
|
|
40467
|
+
return {
|
|
40468
|
+
content: response.status === 202 ? { delivery: "mailbox", operation: result.content } : { delivery: "realtime", result: result.content }
|
|
40469
|
+
};
|
|
40470
|
+
}
|
|
40471
|
+
return result;
|
|
38198
40472
|
}
|
|
38199
40473
|
};
|
|
38200
40474
|
}
|
|
@@ -38229,26 +40503,102 @@ async function handleInvoke(c, env) {
|
|
|
38229
40503
|
const node = resolved.node;
|
|
38230
40504
|
const command = resolved.rest;
|
|
38231
40505
|
if (!check(ctx, node.path, "read").allow) throw TBError.notFound("not found");
|
|
40506
|
+
const toolMarker = deviceToolMarker(node);
|
|
40507
|
+
let invokeRequest;
|
|
40508
|
+
const readInvokeRequest = async () => {
|
|
40509
|
+
if (invokeRequest !== void 0) return await invokeRequest;
|
|
40510
|
+
invokeRequest = (async () => {
|
|
40511
|
+
const parsed = toolMarker === null ? await c.req.json().catch(() => null) : await mailboxJsonObject(c, { allowEmpty: true });
|
|
40512
|
+
if (parsed !== null && (typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
40513
|
+
throw new TBError("invalid_argument", "body must be a JSON object (command arguments)");
|
|
40514
|
+
}
|
|
40515
|
+
const raw2 = { ...parsed ?? {} };
|
|
40516
|
+
const unknownControls = Object.keys(raw2).filter(
|
|
40517
|
+
(key) => key.startsWith("~") && key !== "~delivery"
|
|
40518
|
+
);
|
|
40519
|
+
if (unknownControls.length > 0) {
|
|
40520
|
+
throw new TBError(
|
|
40521
|
+
"invalid_argument",
|
|
40522
|
+
`unknown invocation control '${unknownControls[0]}'`
|
|
40523
|
+
);
|
|
40524
|
+
}
|
|
40525
|
+
const rawDelivery = raw2["~delivery"];
|
|
40526
|
+
delete raw2["~delivery"];
|
|
40527
|
+
if (rawDelivery !== void 0 && rawDelivery !== "realtime" && rawDelivery !== "mailbox" && rawDelivery !== "fallback") {
|
|
40528
|
+
throw new TBError(
|
|
40529
|
+
"invalid_argument",
|
|
40530
|
+
"'~delivery' must be one of realtime, mailbox, fallback"
|
|
40531
|
+
);
|
|
40532
|
+
}
|
|
40533
|
+
return {
|
|
40534
|
+
arguments: raw2,
|
|
40535
|
+
delivery: rawDelivery ?? "realtime",
|
|
40536
|
+
explicitDelivery: rawDelivery !== void 0
|
|
40537
|
+
};
|
|
40538
|
+
})();
|
|
40539
|
+
return await invokeRequest;
|
|
40540
|
+
};
|
|
38232
40541
|
const readInvokeBody = async () => {
|
|
38233
|
-
const
|
|
38234
|
-
if (
|
|
38235
|
-
throw new TBError(
|
|
40542
|
+
const request = await readInvokeRequest();
|
|
40543
|
+
if (request.explicitDelivery) {
|
|
40544
|
+
throw new TBError(
|
|
40545
|
+
"invalid_argument",
|
|
40546
|
+
"'~delivery' is only supported by device-backed tool commands"
|
|
40547
|
+
);
|
|
38236
40548
|
}
|
|
38237
|
-
return
|
|
40549
|
+
return request.arguments;
|
|
38238
40550
|
};
|
|
38239
|
-
const toolMarker = deviceToolMarker(node);
|
|
38240
40551
|
if (toolMarker !== null) {
|
|
38241
40552
|
if (!check(ctx, node.path, "call").allow) {
|
|
38242
40553
|
throw new TBError("permission_denied", `no scope grants 'call' on '${node.path}'`);
|
|
38243
40554
|
}
|
|
38244
|
-
const
|
|
38245
|
-
|
|
40555
|
+
const declared = toolMarker.cmds?.find((item) => item.name.toLowerCase() === command);
|
|
40556
|
+
if (toolMarker.cmds !== void 0 && declared === void 0) throw TBError.notFound("not found");
|
|
40557
|
+
const request = await readInvokeRequest();
|
|
40558
|
+
const capability = declared?.delivery ?? "realtime";
|
|
40559
|
+
const canRealtime = capability === "realtime" || capability === "both";
|
|
40560
|
+
const canMailbox = capability === "mailbox" || capability === "both";
|
|
40561
|
+
if (request.delivery === "realtime" && !canRealtime) {
|
|
40562
|
+
throw new TBError("invalid_argument", "device command does not support realtime delivery");
|
|
40563
|
+
}
|
|
40564
|
+
if (request.delivery === "mailbox" && !canMailbox) {
|
|
40565
|
+
throw new TBError("invalid_argument", "device command does not support mailbox delivery");
|
|
40566
|
+
}
|
|
40567
|
+
if (request.delivery === "fallback" && !canMailbox) {
|
|
40568
|
+
throw new TBError("invalid_argument", "device command does not support mailbox fallback");
|
|
40569
|
+
}
|
|
40570
|
+
const ttlSeconds = request.delivery === "realtime" ? void 0 : deviceOperationTtlSeconds(new URL(c.req.url));
|
|
40571
|
+
const enqueue = async () => await enqueueDeviceCommand(c, env, {
|
|
40572
|
+
arguments: request.arguments,
|
|
40573
|
+
command,
|
|
40574
|
+
marker: toolMarker,
|
|
40575
|
+
node,
|
|
40576
|
+
...ttlSeconds === void 0 ? {} : { ttlSeconds }
|
|
40577
|
+
});
|
|
40578
|
+
if (request.delivery === "mailbox" || !canRealtime) return await enqueue();
|
|
40579
|
+
if (request.delivery === "fallback" && deps.device === void 0) return await enqueue();
|
|
40580
|
+
const attempt2 = await attemptDevice(deps, toolMarker.deviceId, {
|
|
38246
40581
|
// 帧 path 含命令叶子段:<mount 相对路径>/<命令>。
|
|
38247
40582
|
path: `${relativeDevicePath(node.path, toolMarker.mountPath)}/${command}`,
|
|
38248
|
-
arguments:
|
|
40583
|
+
arguments: request.arguments,
|
|
38249
40584
|
context: deviceCallContextFrom(ctx)
|
|
38250
40585
|
});
|
|
38251
|
-
|
|
40586
|
+
if (attempt2.disposition === "not_dispatched" && request.delivery === "fallback") {
|
|
40587
|
+
return await enqueue();
|
|
40588
|
+
}
|
|
40589
|
+
if (attempt2.result.ok) {
|
|
40590
|
+
const response = renderResult(attempt2.result.value, negotiate(c.req.header("accept")));
|
|
40591
|
+
response.headers.set("x-tb-delivery", "realtime");
|
|
40592
|
+
return response;
|
|
40593
|
+
}
|
|
40594
|
+
if (attempt2.disposition === "unknown" && request.delivery === "fallback") {
|
|
40595
|
+
throw new TBError(
|
|
40596
|
+
attempt2.result.error.code,
|
|
40597
|
+
`${attempt2.result.error.message}; delivery outcome is unknown and was not enqueued`,
|
|
40598
|
+
{ retryable: false }
|
|
40599
|
+
);
|
|
40600
|
+
}
|
|
40601
|
+
throw tbErrorFromBody(attempt2.result.error);
|
|
38252
40602
|
}
|
|
38253
40603
|
if ((node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config !== void 0) {
|
|
38254
40604
|
if (!check(ctx, node.path, "call").allow) {
|
|
@@ -38395,6 +40745,7 @@ async function handleInvoke(c, env) {
|
|
|
38395
40745
|
deps
|
|
38396
40746
|
);
|
|
38397
40747
|
assertSecretRefUse(ctx.scopes, cfgPatch);
|
|
40748
|
+
await assertMcpOAuthConfig(cfgPatch, deps.secrets);
|
|
38398
40749
|
await assertContextConfig(cfgPatch, deps);
|
|
38399
40750
|
await assertSkillhubConfig(cfgPatch, deps);
|
|
38400
40751
|
await assertToolConfig(cfgPatch, deps, ctx, targetPath);
|
|
@@ -38490,6 +40841,7 @@ function createRouteEnv(deps) {
|
|
|
38490
40841
|
if (!declared.has("search")) return [];
|
|
38491
40842
|
return [
|
|
38492
40843
|
"search",
|
|
40844
|
+
...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : [],
|
|
38493
40845
|
...declared.has("search:semantic") ? ["search:semantic"] : []
|
|
38494
40846
|
];
|
|
38495
40847
|
};
|
|
@@ -38500,7 +40852,12 @@ function createRouteEnv(deps) {
|
|
|
38500
40852
|
catalogDigest ??= catalogSetDigest(catalog);
|
|
38501
40853
|
return catalogDigest;
|
|
38502
40854
|
};
|
|
38503
|
-
|
|
40855
|
+
let mailboxInstance;
|
|
40856
|
+
const mailbox = () => {
|
|
40857
|
+
mailboxInstance ??= createDeviceMailboxService(deps);
|
|
40858
|
+
return mailboxInstance;
|
|
40859
|
+
};
|
|
40860
|
+
return { builtinsOf, deps, globalSearchCapabilities, mailbox, pluginCatalogDigest, searchSync };
|
|
38504
40861
|
}
|
|
38505
40862
|
|
|
38506
40863
|
// ../app/src/routes/help.ts
|
|
@@ -38526,8 +40883,17 @@ async function handleHelp(c, env) {
|
|
|
38526
40883
|
return renderHelp(await enrichHelp(model2, "", store), rep);
|
|
38527
40884
|
}
|
|
38528
40885
|
if (!check(ctx, path, "read").allow) throw TBError.notFound("not found");
|
|
38529
|
-
const
|
|
38530
|
-
if (
|
|
40886
|
+
const projector = await remotePathProjectorIfMatch(registry, path);
|
|
40887
|
+
if (projector !== null) {
|
|
40888
|
+
const headers = new Headers(c.req.raw.headers);
|
|
40889
|
+
headers.set("accept", "application/json");
|
|
40890
|
+
const remote = await remotePassthroughIfMatch(c, ctx, registry, path, "~help", deps, headers);
|
|
40891
|
+
if (remote === null) throw remoteProtocolError("remote ~help lost its mount owner");
|
|
40892
|
+
if (!remote.ok) return remote;
|
|
40893
|
+
const parsed = helpJsonSchema.safeParse(await remote.json().catch(() => null));
|
|
40894
|
+
if (!parsed.success) throw remoteProtocolError("remote ~help returned invalid JSON");
|
|
40895
|
+
return renderHelp(projector.projectHelp(parsed.data, path, ctx), rep);
|
|
40896
|
+
}
|
|
38531
40897
|
const builtins = builtinsOf(store);
|
|
38532
40898
|
const refresh = c.req.query("refresh") === "1";
|
|
38533
40899
|
const schemas2 = c.req.query("schemas") === "1";
|
|
@@ -38554,18 +40920,45 @@ async function handleHelp(c, env) {
|
|
|
38554
40920
|
|
|
38555
40921
|
// ../app/src/routes/tree.ts
|
|
38556
40922
|
var REMOTE_OPAQUE_KINDS = /* @__PURE__ */ new Set(["remote"]);
|
|
40923
|
+
function renderTree(tree, rep) {
|
|
40924
|
+
if (rep === "json") {
|
|
40925
|
+
return new Response(JSON.stringify(treeJsonSchema.parse(tree)), {
|
|
40926
|
+
headers: { "content-type": contentTypeFor("json") }
|
|
40927
|
+
});
|
|
40928
|
+
}
|
|
40929
|
+
if (rep === "dsl") {
|
|
40930
|
+
return new Response(renderTreeDsl(tree), {
|
|
40931
|
+
headers: { "content-type": contentTypeFor("dsl") }
|
|
40932
|
+
});
|
|
40933
|
+
}
|
|
40934
|
+
return new Response(`\`\`\`text
|
|
40935
|
+
${renderTreeDsl(tree)}\`\`\`
|
|
40936
|
+
`, {
|
|
40937
|
+
headers: { "content-type": contentTypeFor("markdown") }
|
|
40938
|
+
});
|
|
40939
|
+
}
|
|
38557
40940
|
async function handleTree(c, env) {
|
|
38558
40941
|
const { deps } = env;
|
|
38559
40942
|
const path = splitReserved(new URL(c.req.url).pathname, "~tree");
|
|
38560
40943
|
if (path === null) throw TBError.notFound("no such path");
|
|
38561
40944
|
const ctx = c.get("ctx");
|
|
38562
40945
|
const store = c.get("store");
|
|
40946
|
+
const rep = negotiate(c.req.header("accept"));
|
|
38563
40947
|
if (path !== "" && !check(ctx, path, "read").allow) throw TBError.notFound("not found");
|
|
38564
40948
|
const registry = new NodeRegistryStore(store);
|
|
38565
40949
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
38566
40950
|
if (path !== "") {
|
|
38567
|
-
const
|
|
38568
|
-
if (
|
|
40951
|
+
const projector = await remotePathProjectorIfMatch(registry, path);
|
|
40952
|
+
if (projector !== null) {
|
|
40953
|
+
const headers = new Headers(c.req.raw.headers);
|
|
40954
|
+
headers.set("accept", "application/json");
|
|
40955
|
+
const remote = await remotePassthroughIfMatch(c, ctx, registry, path, "~tree", deps, headers);
|
|
40956
|
+
if (remote === null) throw remoteProtocolError("remote ~tree lost its mount owner");
|
|
40957
|
+
if (!remote.ok) return remote;
|
|
40958
|
+
const parsed = treeJsonSchema.safeParse(await remote.json().catch(() => null));
|
|
40959
|
+
if (!parsed.success) throw remoteProtocolError("remote ~tree returned invalid JSON");
|
|
40960
|
+
return renderTree(projector.projectTree(parsed.data, path, ctx), rep);
|
|
40961
|
+
}
|
|
38569
40962
|
}
|
|
38570
40963
|
let rootEntry;
|
|
38571
40964
|
if (path !== "") {
|
|
@@ -38596,22 +40989,7 @@ async function handleTree(c, env) {
|
|
|
38596
40989
|
opaqueKinds: REMOTE_OPAQUE_KINDS,
|
|
38597
40990
|
...rootEntry !== void 0 ? { rootEntry } : {}
|
|
38598
40991
|
});
|
|
38599
|
-
|
|
38600
|
-
if (rep === "json") {
|
|
38601
|
-
return new Response(JSON.stringify(treeJsonSchema.parse(tree)), {
|
|
38602
|
-
headers: { "content-type": contentTypeFor("json") }
|
|
38603
|
-
});
|
|
38604
|
-
}
|
|
38605
|
-
if (rep === "dsl") {
|
|
38606
|
-
return new Response(renderTreeDsl(tree), {
|
|
38607
|
-
headers: { "content-type": contentTypeFor("dsl") }
|
|
38608
|
-
});
|
|
38609
|
-
}
|
|
38610
|
-
return new Response(`\`\`\`text
|
|
38611
|
-
${renderTreeDsl(tree)}\`\`\`
|
|
38612
|
-
`, {
|
|
38613
|
-
headers: { "content-type": contentTypeFor("markdown") }
|
|
38614
|
-
});
|
|
40992
|
+
return renderTree(tree, rep);
|
|
38615
40993
|
}
|
|
38616
40994
|
|
|
38617
40995
|
// ../app/src/tbApp.ts
|
|
@@ -38683,6 +41061,13 @@ function createTbApp(deps) {
|
|
|
38683
41061
|
async (c) => await runHandler(async () => {
|
|
38684
41062
|
const segs = new URL(c.req.url).pathname.replace(/\/+$/, "").split("/");
|
|
38685
41063
|
const last = segs.pop() ?? "";
|
|
41064
|
+
const pathname = new URL(c.req.url).pathname.replace(/\/+$/, "");
|
|
41065
|
+
if (pathname.startsWith("/~device/operations/")) {
|
|
41066
|
+
return await handleDeviceMailboxControl(c, env);
|
|
41067
|
+
}
|
|
41068
|
+
if (pathname.startsWith("/~device/mailbox/")) {
|
|
41069
|
+
return await handleDeviceMailboxData(c, env);
|
|
41070
|
+
}
|
|
38686
41071
|
if (last === "~register") return await handleRegister(c, env);
|
|
38687
41072
|
if (last === "~authorize") return await handleAuthorize(c, env);
|
|
38688
41073
|
if (last === "~feedback" || segs[segs.length - 1] === "~feedback") {
|
|
@@ -38773,7 +41158,10 @@ var DeviceHub = class {
|
|
|
38773
41158
|
*/
|
|
38774
41159
|
async invoke(deviceId, req) {
|
|
38775
41160
|
if (this.activeByDevice.has(deviceId)) return await this.invokeLocal(deviceId, req);
|
|
38776
|
-
const offline = {
|
|
41161
|
+
const offline = {
|
|
41162
|
+
disposition: "not_dispatched",
|
|
41163
|
+
result: { ok: false, error: TBError.deviceOffline().toJSON() }
|
|
41164
|
+
};
|
|
38777
41165
|
if (this.router === void 0) return offline;
|
|
38778
41166
|
return await this.router.forward(deviceId, req) ?? offline;
|
|
38779
41167
|
}
|
|
@@ -38784,7 +41172,10 @@ var DeviceHub = class {
|
|
|
38784
41172
|
* 那会再次查路由并转发,在两个副本间弹来弹去。这里没有连接就直接 offline。
|
|
38785
41173
|
*/
|
|
38786
41174
|
async invokeLocal(deviceId, req) {
|
|
38787
|
-
const offline = {
|
|
41175
|
+
const offline = {
|
|
41176
|
+
disposition: "not_dispatched",
|
|
41177
|
+
result: { ok: false, error: TBError.deviceOffline().toJSON() }
|
|
41178
|
+
};
|
|
38788
41179
|
const conn = this.activeByDevice.get(deviceId);
|
|
38789
41180
|
if (conn === void 0) return offline;
|
|
38790
41181
|
const authCtx = await identify(this.store, conn.authorization, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -38803,7 +41194,10 @@ var DeviceHub = class {
|
|
|
38803
41194
|
return offline;
|
|
38804
41195
|
}
|
|
38805
41196
|
if (this.activeByDevice.get(deviceId) !== conn) return offline;
|
|
38806
|
-
return await new Promise((resolve3) => conn.session.call(
|
|
41197
|
+
return await new Promise((resolve3) => conn.session.call(
|
|
41198
|
+
req,
|
|
41199
|
+
(result, disposition) => resolve3({ disposition, result })
|
|
41200
|
+
));
|
|
38807
41201
|
}
|
|
38808
41202
|
/** DeviceChannel.ws:Node 宿主的升级在 http 层处理,永不应命中此路由。 */
|
|
38809
41203
|
async ws() {
|
|
@@ -39080,7 +41474,13 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
39080
41474
|
return await new Promise((resolve3) => {
|
|
39081
41475
|
const timer = setTimeout(() => {
|
|
39082
41476
|
this.pending.delete(correlationId);
|
|
39083
|
-
resolve3({
|
|
41477
|
+
resolve3({
|
|
41478
|
+
disposition: "unknown",
|
|
41479
|
+
result: {
|
|
41480
|
+
ok: false,
|
|
41481
|
+
error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u8D85\u65F6").toJSON()
|
|
41482
|
+
}
|
|
41483
|
+
});
|
|
39084
41484
|
}, this.opts.forwardTimeoutMs ?? DEVICE_FORWARD_TIMEOUT_MS);
|
|
39085
41485
|
timer.unref?.();
|
|
39086
41486
|
this.pending.set(correlationId, { resolve: resolve3, timer });
|
|
@@ -39090,8 +41490,11 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
39090
41490
|
this.pending.delete(correlationId);
|
|
39091
41491
|
clearTimeout(waiter.timer);
|
|
39092
41492
|
waiter.resolve({
|
|
39093
|
-
|
|
39094
|
-
|
|
41493
|
+
disposition: "unknown",
|
|
41494
|
+
result: {
|
|
41495
|
+
ok: false,
|
|
41496
|
+
error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u5931\u8D25").toJSON()
|
|
41497
|
+
}
|
|
39095
41498
|
});
|
|
39096
41499
|
});
|
|
39097
41500
|
});
|
|
@@ -39101,7 +41504,13 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
39101
41504
|
this.renewTimer = void 0;
|
|
39102
41505
|
for (const [, waiter] of this.pending) {
|
|
39103
41506
|
clearTimeout(waiter.timer);
|
|
39104
|
-
waiter.resolve({
|
|
41507
|
+
waiter.resolve({
|
|
41508
|
+
disposition: "unknown",
|
|
41509
|
+
result: {
|
|
41510
|
+
ok: false,
|
|
41511
|
+
error: new TBError("unavailable", "\u7F51\u5173\u6B63\u5728\u5173\u95ED").toJSON()
|
|
41512
|
+
}
|
|
41513
|
+
});
|
|
39105
41514
|
}
|
|
39106
41515
|
this.pending.clear();
|
|
39107
41516
|
await Promise.all([...this.owned].map(async (id) => await this.release(id)));
|
|
@@ -39130,7 +41539,13 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
39130
41539
|
try {
|
|
39131
41540
|
result = await this.opts.onLocalCall(call.deviceId, call.req);
|
|
39132
41541
|
} catch {
|
|
39133
|
-
result = {
|
|
41542
|
+
result = {
|
|
41543
|
+
disposition: "unknown",
|
|
41544
|
+
result: {
|
|
41545
|
+
ok: false,
|
|
41546
|
+
error: new TBError("internal", "\u8BBE\u5907\u8C03\u7528\u6267\u884C\u5931\u8D25").toJSON()
|
|
41547
|
+
}
|
|
41548
|
+
};
|
|
39134
41549
|
}
|
|
39135
41550
|
const reply = { correlationId: call.correlationId, result };
|
|
39136
41551
|
await this.backend.publish(deviceReplyChannel(call.replyTo), JSON.stringify(reply)).catch(() => {
|
|
@@ -39414,6 +41829,41 @@ var FsObjectStore = class {
|
|
|
39414
41829
|
// ../core/src/node/shellExecutor.ts
|
|
39415
41830
|
import { spawn as nodeSpawn } from "child_process";
|
|
39416
41831
|
var SHELL_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
41832
|
+
var SHELL_EXEC_DEFAULT_TIMEOUT_MS = 55e3;
|
|
41833
|
+
|
|
41834
|
+
// ../core/src/node/structuredCommand.ts
|
|
41835
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
41836
|
+
var STRUCTURED_COMMAND_PROFILE_VERSION = 1;
|
|
41837
|
+
var STRUCTURED_COMMAND_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
41838
|
+
var envNameSchema = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
41839
|
+
var argumentSchema = external_exports.strictObject({
|
|
41840
|
+
input: external_exports.string().min(1),
|
|
41841
|
+
description: external_exports.string().min(1).optional(),
|
|
41842
|
+
flag: external_exports.string().min(1).optional(),
|
|
41843
|
+
choices: external_exports.array(external_exports.string()).min(1).optional(),
|
|
41844
|
+
multiple: external_exports.boolean().optional(),
|
|
41845
|
+
required: external_exports.boolean().optional(),
|
|
41846
|
+
type: external_exports.enum(["string", "number", "boolean"]).optional()
|
|
41847
|
+
});
|
|
41848
|
+
var commandSchema = external_exports.strictObject({
|
|
41849
|
+
name: external_exports.string().min(1),
|
|
41850
|
+
description: external_exports.string().min(1),
|
|
41851
|
+
executable: external_exports.string().min(1),
|
|
41852
|
+
argv: external_exports.array(external_exports.union([external_exports.string(), argumentSchema])).optional(),
|
|
41853
|
+
cwd: external_exports.string().min(1).optional(),
|
|
41854
|
+
effect: external_exports.enum(["read", "write", "destructive"]),
|
|
41855
|
+
delivery: external_exports.enum(["realtime", "mailbox", "both"]).optional(),
|
|
41856
|
+
confirm: external_exports.boolean().optional(),
|
|
41857
|
+
timeoutMs: external_exports.number().int().positive().max(SHELL_EXEC_DEFAULT_TIMEOUT_MS).optional(),
|
|
41858
|
+
maxOutputBytes: external_exports.number().int().positive().max(STRUCTURED_COMMAND_MAX_OUTPUT_BYTES).optional(),
|
|
41859
|
+
inheritEnv: external_exports.array(envNameSchema).optional()
|
|
41860
|
+
});
|
|
41861
|
+
var profileSchema = external_exports.strictObject({
|
|
41862
|
+
version: external_exports.literal(STRUCTURED_COMMAND_PROFILE_VERSION),
|
|
41863
|
+
path: external_exports.string().min(1),
|
|
41864
|
+
description: external_exports.string().min(1),
|
|
41865
|
+
commands: external_exports.array(commandSchema).min(1)
|
|
41866
|
+
});
|
|
39417
41867
|
|
|
39418
41868
|
// src/objects.ts
|
|
39419
41869
|
import { dirname as dirname2, join as join2, sep as sep2 } from "path";
|
|
@@ -39697,7 +42147,7 @@ var PgSearchDriver = class {
|
|
|
39697
42147
|
records,
|
|
39698
42148
|
PG_SEARCH_TOOLS_TABLE,
|
|
39699
42149
|
PG_SEARCH_TOOLS_COLUMNS,
|
|
39700
|
-
(record2) => [record2.path, record2.name, record2.description, record2.feedback]
|
|
42150
|
+
(record2) => [record2.path, record2.name, record2.description, record2.effect, record2.feedback]
|
|
39701
42151
|
);
|
|
39702
42152
|
}
|
|
39703
42153
|
insertSnapshots(digests) {
|
|
@@ -39887,7 +42337,7 @@ var SqliteSearchDriver = class {
|
|
|
39887
42337
|
}
|
|
39888
42338
|
insertRecords(records) {
|
|
39889
42339
|
return records.map((record2) => ({
|
|
39890
|
-
params: [record2.path, record2.name, record2.description, record2.feedback],
|
|
42340
|
+
params: [record2.path, record2.name, record2.description, record2.effect, record2.feedback],
|
|
39891
42341
|
sql: TOOL_SEARCH_INSERT_SQL
|
|
39892
42342
|
}));
|
|
39893
42343
|
}
|
|
@@ -40031,7 +42481,7 @@ import { join as join4 } from "path";
|
|
|
40031
42481
|
// package.json
|
|
40032
42482
|
var package_default = {
|
|
40033
42483
|
name: "@tool-bridge/server",
|
|
40034
|
-
version: "0.
|
|
42484
|
+
version: "0.20.0",
|
|
40035
42485
|
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
42486
|
type: "module",
|
|
40037
42487
|
license: "MIT",
|
|
@@ -40370,26 +42820,27 @@ function createTbServer(config) {
|
|
|
40370
42820
|
}
|
|
40371
42821
|
const app = createTbApp(deps);
|
|
40372
42822
|
let server;
|
|
40373
|
-
let
|
|
40374
|
-
let
|
|
40375
|
-
const
|
|
40376
|
-
if (
|
|
40377
|
-
|
|
42823
|
+
let maintenanceTimer;
|
|
42824
|
+
let maintenanceInFlight = false;
|
|
42825
|
+
const runMaintenance = async () => {
|
|
42826
|
+
if (maintenanceInFlight) return;
|
|
42827
|
+
maintenanceInFlight = true;
|
|
40378
42828
|
try {
|
|
40379
42829
|
await cleanupDefaultStore(deps);
|
|
42830
|
+
if (deps.encryptionKey !== void 0) await cleanupDeviceMailbox(deps);
|
|
40380
42831
|
} finally {
|
|
40381
|
-
|
|
42832
|
+
maintenanceInFlight = false;
|
|
40382
42833
|
}
|
|
40383
42834
|
};
|
|
40384
42835
|
const reportStoreCleanupFailure = () => {
|
|
40385
42836
|
console.warn(JSON.stringify({ event: "tool_bridge_store_cleanup_failed" }));
|
|
40386
42837
|
};
|
|
40387
42838
|
const scheduleStoreCleanup = () => {
|
|
40388
|
-
void
|
|
40389
|
-
|
|
40390
|
-
void
|
|
42839
|
+
void runMaintenance().catch(reportStoreCleanupFailure);
|
|
42840
|
+
maintenanceTimer = setInterval(() => {
|
|
42841
|
+
void runMaintenance().catch(reportStoreCleanupFailure);
|
|
40391
42842
|
}, config.storeCleanupIntervalSec * 1e3);
|
|
40392
|
-
|
|
42843
|
+
maintenanceTimer.unref?.();
|
|
40393
42844
|
};
|
|
40394
42845
|
return {
|
|
40395
42846
|
app,
|
|
@@ -40418,9 +42869,9 @@ function createTbServer(config) {
|
|
|
40418
42869
|
},
|
|
40419
42870
|
async close() {
|
|
40420
42871
|
draining = true;
|
|
40421
|
-
if (
|
|
40422
|
-
clearInterval(
|
|
40423
|
-
|
|
42872
|
+
if (maintenanceTimer !== void 0) {
|
|
42873
|
+
clearInterval(maintenanceTimer);
|
|
42874
|
+
maintenanceTimer = void 0;
|
|
40424
42875
|
}
|
|
40425
42876
|
let closed;
|
|
40426
42877
|
if (server !== void 0) {
|