@tool-bridge/server 0.19.0 → 0.21.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-23JBIM6X.js → airtable-DMJEEGJP.js} +19 -16
- package/dist/{alt_text_generator_ai-ADWBTCIH.js → alt_text_generator_ai-RELFQ5FV.js} +4 -4
- package/dist/{amap-CYQCYW2Z.js → amap-YSYEACKQ.js} +4 -4
- package/dist/{anthropic-5P62HDNR.js → anthropic-D5X5GSYE.js} +23 -25
- package/dist/{apify-54RZUAGW.js → apify-5NVFU3IZ.js} +20 -19
- package/dist/{brave_search-AHJ6FTD5.js → brave_search-X557FMGO.js} +16 -14
- package/dist/chunk-JEWFX6Z6.js +71 -0
- package/dist/{chunk-CLEZ6YQ6.js → chunk-MDY7K5CX.js} +1090 -606
- package/dist/{chunk-OKRZPQ3R.js → chunk-MZCIPUNQ.js} +1002 -242
- package/dist/{chunk-VLOIDU5U.js → chunk-Q74GLIDK.js} +3 -3
- package/dist/{chunk-B4P6YZFS.js → chunk-QR4LK3HF.js} +1 -1
- package/dist/{chunk-SWBNHIO4.js → chunk-ZTTN6AJG.js} +1 -1
- package/dist/{circleci-UM3NSQJ3.js → circleci-3P3QTUX2.js} +20 -24
- package/dist/{clerk-FE5LQ2CZ.js → clerk-GNMZQ2MY.js} +22 -21
- package/dist/{cloudflare_dns-KEZABKUT.js → cloudflare_dns-VWZH3ZTR.js} +21 -17
- package/dist/{cohere-4IPJADXP.js → cohere-C7QVIIMG.js} +19 -17
- package/dist/{confluence-56QEKNQI.js → confluence-52I37IUT.js} +16 -11
- package/dist/{convertapi-3QRM5K5T.js → convertapi-3TRJ3WEO.js} +3 -3
- package/dist/{currencyapi-L6G4OND5.js → currencyapi-LWEANWVI.js} +23 -26
- package/dist/{deepseek-LGC6HBFG.js → deepseek-5U24YG4P.js} +27 -17
- package/dist/{docker_hub-XDMK3BIU.js → docker_hub-CMISLRSE.js} +4 -4
- package/dist/{dropbox-4QARMFZU.js → dropbox-TW4QN6FT.js} +9 -8
- package/dist/{dub-C6JSJKOT.js → dub-EFXGI5UH.js} +22 -19
- package/dist/{e2b-6A6TVACJ.js → e2b-PHM63OVF.js} +20 -20
- package/dist/{exa-SIWWW6AA.js → exa-DYOQWURR.js} +22 -25
- package/dist/{fal_ai-3IJXOKSP.js → fal_ai-VJPMCNRH.js} +18 -17
- package/dist/{feishu-L4KDA4DC.js → feishu-TFIZXPIN.js} +2 -2
- package/dist/{feishu_custom_bot-SDM75H4P.js → feishu_custom_bot-ADSE23KU.js} +3 -3
- package/dist/{figma-6DD436DN.js → figma-4AK47AYY.js} +21 -23
- package/dist/{firehydrant-SYSGYB3R.js → firehydrant-RGPTW4EA.js} +15 -15
- package/dist/{fixer-FZG7ASTF.js → fixer-OAMF4DKP.js} +4 -4
- package/dist/{fly-HMRCZBUC.js → fly-YB4AQD4O.js} +20 -16
- package/dist/{front-FAFSD7I3.js → front-6GTVYKUM.js} +20 -23
- package/dist/{geocodio-4D3TWENM.js → geocodio-YRT4K25M.js} +4 -4
- package/dist/{ghost-OSO2FOZS.js → ghost-JCW6UIP2.js} +4 -4
- package/dist/{github-OR4I4RHE.js → github-CEGVRMNX.js} +28 -27
- package/dist/{gmail-5AOPJ63E.js → gmail-JCMJD55P.js} +17 -17
- package/dist/{googlecalendar-SOJIJUWH.js → googlecalendar-2EAW4MNM.js} +16 -14
- package/dist/{googledocs-QSKN5TLN.js → googledocs-YNKKBM6Q.js} +3 -3
- package/dist/{grafana-R3AIZM7M.js → grafana-353VSGXN.js} +24 -25
- package/dist/{graphhopper-4CR3QOAX.js → graphhopper-H3IVJNDS.js} +4 -4
- package/dist/index.d.ts +52 -36
- package/dist/index.js +3 -3
- package/dist/{ip2proxy-OBCG6X6J.js → ip2proxy-DBGRWH7S.js} +4 -4
- package/dist/{ipgeolocation_io-NHH3DTQW.js → ipgeolocation_io-5I2BDVMG.js} +4 -4
- package/dist/{ipqualityscore-7POMCBEC.js → ipqualityscore-ILV7TQCV.js} +4 -4
- package/dist/{jina_ai-YXVKROV4.js → jina_ai-L5BH455V.js} +20 -21
- package/dist/{jira-LADO3VJU.js → jira-IGTILUZA.js} +20 -16
- package/dist/{langsmith-XZ6UAQFU.js → langsmith-BYRX4AOF.js} +25 -25
- package/dist/{lemlist-P74GLNE7.js → lemlist-ZGNYTIQO.js} +23 -24
- package/dist/{linear-CXJ7D37A.js → linear-HLGOWISN.js} +3 -3
- package/dist/{logsnag-EVVHWFLK.js → logsnag-5RGTMTU2.js} +20 -22
- package/dist/main.js +102 -102
- package/dist/{mattermost-WG2MRGBD.js → mattermost-BONGXVC7.js} +21 -17
- package/dist/{mem0-HQWUKG6U.js → mem0-RUVNUPTE.js} +15 -15
- package/dist/{memos-SQMJIFOE.js → memos-TED4HI54.js} +36 -32
- package/dist/{metabase-E27G2DZI.js → metabase-ZHXHRHZK.js} +22 -18
- package/dist/{mistral_ai-MIL63NIT.js → mistral_ai-NGP5SHU7.js} +24 -21
- package/dist/{mother_duck-AIZVCBNR.js → mother_duck-TKVQM35M.js} +23 -19
- package/dist/{ngrok-XS4DVIQS.js → ngrok-BYCUJ7QV.js} +17 -22
- package/dist/{notes-JEOUSL2Y.js → notes-KXRNNYDW.js} +1 -1
- package/dist/{notion-U4EZVDW2.js → notion-2ILYKJOO.js} +20 -17
- package/dist/{open_exchange_rates-AZSQEYEJ.js → open_exchange_rates-JU4DC6HZ.js} +4 -4
- package/dist/{openai-HW3SYMTQ.js → openai-6SOT26ZK.js} +22 -16
- package/dist/{opengraph_io-QX74MAEL.js → opengraph_io-ZEFHV2OL.js} +4 -4
- package/dist/{openrouter-OXSKYL6N.js → openrouter-R4P7JL5L.js} +19 -14
- package/dist/{outline-CCQAPDSW.js → outline-VCTQ5V2X.js} +18 -20
- package/dist/{paddle-6KQPSSCK.js → paddle-AZOAKBJS.js} +17 -15
- package/dist/{perplexity-2TXO7IQE.js → perplexity-OMRLGVAT.js} +19 -23
- package/dist/{pinecone-YOO7NULI.js → pinecone-DI62W3ZX.js} +19 -24
- package/dist/{pipedrive-Q6RIFIFP.js → pipedrive-CW7HONYW.js} +22 -23
- package/dist/{polar-KUMGCES3.js → polar-5DRGTXNR.js} +19 -17
- package/dist/{posthog-VXQHBSMB.js → posthog-3DTHFCKE.js} +29 -24
- package/dist/{postmark-YLIPF52S.js → postmark-ME2V2XXI.js} +19 -17
- package/dist/{prerender-CRY7UJN5.js → prerender-5EAY7GDM.js} +4 -4
- package/dist/{pubmed-3Z4T5AM7.js → pubmed-GRQUBGVA.js} +3 -3
- package/dist/{railway-BP37IGFL.js → railway-FTM75TMX.js} +3 -3
- package/dist/{readwise-HTKICF4K.js → readwise-IJQGV6IQ.js} +16 -15
- package/dist/{render-WIVTUXJH.js → render-TMSPHRTY.js} +21 -20
- package/dist/{replicate-JJYPA43D.js → replicate-RJCMDB7H.js} +20 -21
- package/dist/{resend-TKHZVG7A.js → resend-YYN22NIL.js} +15 -11
- package/dist/{rootly-FRSZLG5U.js → rootly-7RXE4K7L.js} +19 -18
- package/dist/{scrapfly-M5WXGQWE.js → scrapfly-ZJJ6XM4Q.js} +4 -4
- package/dist/{scrapingbee-PMXNLLBK.js → scrapingbee-QNGEOD6D.js} +4 -4
- package/dist/{screenshot_fyi-HFW4TAOH.js → screenshot_fyi-SPZAABVD.js} +4 -4
- package/dist/{semantic_scholar-KXVH2VZQ.js → semantic_scholar-SDPVGDDI.js} +21 -20
- package/dist/{sentry-WWOT2LRX.js → sentry-SJ5NN4C4.js} +22 -19
- package/dist/{shodan-FY635H35.js → shodan-YMGOY7KW.js} +4 -4
- package/dist/{shopify-HQLBVSA5.js → shopify-SU5ABP54.js} +23 -19
- package/dist/{stripe-GJN2GOHB.js → stripe-JAC425KE.js} +3 -3
- package/dist/{supabase-BBNZGQU6.js → supabase-GQ2OFVIL.js} +18 -17
- package/dist/{tavily-DPENIBOG.js → tavily-Y7MDA5CI.js} +20 -20
- package/dist/{telegram-L4NONG6H.js → telegram-OIDT3ZLH.js} +4 -4
- package/dist/{telnyx-H67MAEZS.js → telnyx-4JGMC3K7.js} +19 -19
- package/dist/{todoist-G2BSP4DX.js → todoist-O7S2OUGX.js} +18 -17
- package/dist/{trello-7WMVFDG2.js → trello-3LDE4TAQ.js} +4 -4
- package/dist/{turso-ZUAESFC2.js → turso-MUQ44KCA.js} +23 -30
- package/dist/{twilio-EYFYIX3B.js → twilio-VYF3HUU6.js} +3 -3
- package/dist/{umami-AXAUDUVF.js → umami-SVQIK26J.js} +23 -28
- package/dist/{unsplash-UCQAWPLR.js → unsplash-333XYDCJ.js} +19 -18
- package/dist/{upstash_redis-ZPZ4ASU6.js → upstash_redis-IUM2HDLU.js} +17 -13
- package/dist/{uptimerobot-4QVKKUD5.js → uptimerobot-7ZRU2NJV.js} +3 -3
- package/dist/{v2ex-VGEFJTYC.js → v2ex-AVSJCQPS.js} +35 -18
- package/dist/{vercel-JME4TSCN.js → vercel-J7RGXNSA.js} +21 -18
- package/dist/{wolfram_alpha_api-TRGU2MDG.js → wolfram_alpha_api-6CRVUZOJ.js} +4 -4
- package/dist/{wordpress-WRUKLCZR.js → wordpress-THOJLPKA.js} +21 -17
- package/dist/{workos-ZDW4EHS4.js → workos-ENTGRWF2.js} +26 -27
- package/dist/{zhihu-4VI67CWU.js → zhihu-W2BXICTX.js} +21 -21
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createGuardedFetch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZTTN6AJG.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,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
any,
|
|
55
56
|
appendVia,
|
|
56
57
|
array,
|
|
58
|
+
assertNoCollision,
|
|
57
59
|
assertPluginPayloadSize,
|
|
58
60
|
assertPresignTtlSec,
|
|
59
61
|
assertSecretRefUse,
|
|
@@ -90,6 +92,16 @@ import {
|
|
|
90
92
|
derivePresence,
|
|
91
93
|
deviceDirectoryHelpModel,
|
|
92
94
|
deviceFsHelpModel,
|
|
95
|
+
deviceOperationClaimRequestSchema,
|
|
96
|
+
deviceOperationClaimResponseSchema,
|
|
97
|
+
deviceOperationCompleteRequestSchema,
|
|
98
|
+
deviceOperationDetailSchema,
|
|
99
|
+
deviceOperationIdentityRequestSchema,
|
|
100
|
+
deviceOperationListRequestSchema,
|
|
101
|
+
deviceOperationListResponseSchema,
|
|
102
|
+
deviceOperationRenewRequestSchema,
|
|
103
|
+
deviceOperationRenewResponseSchema,
|
|
104
|
+
deviceOperationSummarySchema,
|
|
93
105
|
deviceShellHelpModel,
|
|
94
106
|
discriminatedUnion,
|
|
95
107
|
dispatchContextCmd,
|
|
@@ -185,7 +197,7 @@ import {
|
|
|
185
197
|
url,
|
|
186
198
|
validatePath,
|
|
187
199
|
virtualizeTools
|
|
188
|
-
} from "./chunk-
|
|
200
|
+
} from "./chunk-MZCIPUNQ.js";
|
|
189
201
|
|
|
190
202
|
// src/config.ts
|
|
191
203
|
var DEFAULT_PORT = 8787;
|
|
@@ -810,300 +822,48 @@ function createS3ObjectStore(cfg, opts) {
|
|
|
810
822
|
};
|
|
811
823
|
}
|
|
812
824
|
|
|
813
|
-
// ../app/src/
|
|
814
|
-
var
|
|
815
|
-
function toolCacheKey(path) {
|
|
816
|
-
return `${KEY_PREFIX}${path}`;
|
|
817
|
-
}
|
|
818
|
-
function isCached(v) {
|
|
819
|
-
return typeof v === "object" && v !== null && Array.isArray(v.tools) && typeof v.fetchedAt === "string";
|
|
820
|
-
}
|
|
821
|
-
function isFresh(fetchedAt, ttl, now) {
|
|
822
|
-
const age = (Date.parse(now) - Date.parse(fetchedAt)) / 1e3;
|
|
823
|
-
return Number.isFinite(age) && age >= 0 && age < ttl;
|
|
824
|
-
}
|
|
825
|
-
async function invalidateToolCache(store, path) {
|
|
826
|
-
await store.delete(toolCacheKey(path));
|
|
827
|
-
}
|
|
828
|
-
function cachedTools(value) {
|
|
829
|
-
return isCached(value) ? value.tools : null;
|
|
830
|
-
}
|
|
831
|
-
async function peekToolCache(store, path) {
|
|
832
|
-
return cachedTools(await store.get(toolCacheKey(path)));
|
|
833
|
-
}
|
|
834
|
-
async function getTools(store, path, fetchList, opts) {
|
|
835
|
-
if (!opts.refresh) {
|
|
836
|
-
const raw = await store.get(toolCacheKey(path));
|
|
837
|
-
if (isCached(raw) && isFresh(raw.fetchedAt, opts.ttl, opts.now)) {
|
|
838
|
-
return raw.tools;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
await opts.beforeFresh?.();
|
|
842
|
-
try {
|
|
843
|
-
const tools = await fetchList();
|
|
844
|
-
const record2 = { tools, fetchedAt: opts.now };
|
|
845
|
-
await store.put(toolCacheKey(path), record2);
|
|
846
|
-
await opts.onFresh?.(tools);
|
|
847
|
-
return tools;
|
|
848
|
-
} catch (error) {
|
|
849
|
-
await opts.onFreshError?.();
|
|
850
|
-
throw error;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
825
|
+
// ../app/src/deps.ts
|
|
826
|
+
var TOOL_CACHE_TTL_DEFAULT = 300;
|
|
853
827
|
|
|
854
|
-
// ../app/src/
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const index = value;
|
|
860
|
-
return typeof index.replace === "function" && typeof index.remove === "function" && typeof index.removePrefix === "function" && typeof index.rebuild === "function" && typeof index.initialized === "function";
|
|
861
|
-
}
|
|
862
|
-
function toolNode(node) {
|
|
863
|
-
return (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config?.kind === node.kind;
|
|
864
|
-
}
|
|
865
|
-
function httpTools(node) {
|
|
866
|
-
if (node.config?.kind !== "http") return null;
|
|
867
|
-
return node.config.tools.map((tool) => {
|
|
868
|
-
const spec = {
|
|
869
|
-
description: tool.description,
|
|
870
|
-
effect: effectFor(tool),
|
|
871
|
-
name: tool.name
|
|
872
|
-
};
|
|
873
|
-
if (tool.inputSchema !== void 0) spec.inputSchema = tool.inputSchema;
|
|
874
|
-
return spec;
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
function deviceTools(node) {
|
|
878
|
-
if (node.config?.kind !== "tool") return null;
|
|
879
|
-
const cmds = node.config.providerConfig?.cmds;
|
|
880
|
-
if (!Array.isArray(cmds)) return null;
|
|
881
|
-
return cmds;
|
|
882
|
-
}
|
|
883
|
-
async function canonicalSearchTools(state, nodes) {
|
|
884
|
-
const unique = new Map(nodes.map((node) => [node.path, node]));
|
|
885
|
-
const dynamicPaths = [...unique.values()].filter((node) => node.kind !== "http" && deviceTools(node) === null).map((node) => node.path);
|
|
886
|
-
const cached = dynamicPaths.length === 0 ? /* @__PURE__ */ new Map() : await state.getMany(dynamicPaths.map(toolCacheKey));
|
|
887
|
-
const result = /* @__PURE__ */ new Map();
|
|
888
|
-
for (const node of unique.values()) {
|
|
889
|
-
const tools = node.kind === "http" ? httpTools(node) : deviceTools(node) ?? cachedTools(cached.get(toolCacheKey(node.path)));
|
|
890
|
-
if (tools !== null) result.set(node.path, tools);
|
|
828
|
+
// ../app/src/cursorCleanup.ts
|
|
829
|
+
function positiveInt(value, fallback, field) {
|
|
830
|
+
const actual = value ?? fallback;
|
|
831
|
+
if (!Number.isSafeInteger(actual) || actual < 1) {
|
|
832
|
+
throw new TBError("invalid_argument", `${field} must be a positive integer`);
|
|
891
833
|
}
|
|
892
|
-
return
|
|
893
|
-
}
|
|
894
|
-
function documentsFor(path, tools, feedback) {
|
|
895
|
-
return tools.map((tool) => ({ path, tool, ...feedback === "" ? {} : { feedback } }));
|
|
834
|
+
return actual;
|
|
896
835
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
createdAt: Date.now(),
|
|
911
|
-
// Marker 只是审计提示,正确性由每次 canonical audit 保证。每类固定一个 key,
|
|
912
|
-
// 避免 overflow/device hello 用 UUID marker 把 KV 与扫描成本无界放大。
|
|
913
|
-
key: `${kind === "node" ? DIRTY_NODE_PREFIX : DIRTY_SUBTREE_PREFIX}pending`,
|
|
914
|
-
kind,
|
|
915
|
-
path: canonical
|
|
916
|
-
};
|
|
917
|
-
await this.state.put(marker.key, marker);
|
|
918
|
-
return marker;
|
|
919
|
-
}
|
|
920
|
-
async markNode(path) {
|
|
921
|
-
return await this.mark(path, "node");
|
|
922
|
-
}
|
|
923
|
-
async markSubtree(path) {
|
|
924
|
-
return await this.mark(path, "subtree");
|
|
925
|
-
}
|
|
926
|
-
async abort(marker) {
|
|
927
|
-
if (marker !== void 0 && marker.expectedDigest === void 0) {
|
|
928
|
-
await this.state.delete(marker.key);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
async dirtyMarkers() {
|
|
932
|
-
const markers = [];
|
|
933
|
-
for (const prefix of [DIRTY_NODE_PREFIX, DIRTY_SUBTREE_PREFIX]) {
|
|
934
|
-
let cursor;
|
|
935
|
-
do {
|
|
936
|
-
const page = await this.state.list(prefix, {
|
|
937
|
-
limit: 200,
|
|
938
|
-
...cursor === void 0 ? {} : { cursor }
|
|
939
|
-
});
|
|
940
|
-
for (const item of page.items) {
|
|
941
|
-
const marker = item.value;
|
|
942
|
-
if (typeof marker.createdAt === "number" && marker.key === item.key && (marker.kind === "node" || marker.kind === "subtree") && typeof marker.path === "string") {
|
|
943
|
-
markers.push(marker);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
cursor = page.cursor;
|
|
947
|
-
} while (cursor !== void 0);
|
|
948
|
-
}
|
|
949
|
-
return markers;
|
|
950
|
-
}
|
|
951
|
-
async commitMarker(marker, expectedDigest) {
|
|
952
|
-
if (marker === void 0) return;
|
|
953
|
-
const committed = {
|
|
954
|
-
...marker,
|
|
955
|
-
committedAt: Date.now(),
|
|
956
|
-
expectedDigest
|
|
957
|
-
};
|
|
958
|
-
await this.state.put(marker.key, committed);
|
|
959
|
-
Object.assign(marker, committed);
|
|
960
|
-
}
|
|
961
|
-
async finishMarker(marker) {
|
|
962
|
-
if (marker !== void 0) await this.state.delete(marker.key);
|
|
963
|
-
}
|
|
964
|
-
async feedbackText(path, snapshot) {
|
|
965
|
-
if (snapshot !== void 0) return selectFeedbackSearchText(snapshot);
|
|
966
|
-
return selectFeedbackSearchText(parseFeedbackEntries(await this.state.get(KEY_FEEDBACK + path)));
|
|
967
|
-
}
|
|
968
|
-
async rawTools(node, override) {
|
|
969
|
-
if (override !== void 0) return [...override];
|
|
970
|
-
if (node.kind === "http") return httpTools(node);
|
|
971
|
-
const device = deviceTools(node);
|
|
972
|
-
if (device !== null) return device;
|
|
973
|
-
if (node.kind === "mcp" || node.kind === "tool") {
|
|
974
|
-
return await peekToolCache(this.state, node.path);
|
|
975
|
-
}
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
indexableRecords(path, tools, feedback) {
|
|
979
|
-
try {
|
|
980
|
-
return serializeToolSearchSnapshot(path, tools, feedback);
|
|
981
|
-
} catch (error) {
|
|
982
|
-
if (isTBError(error)) return null;
|
|
983
|
-
throw error;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
async reconcileNode(path, opts = {}) {
|
|
987
|
-
const canonical = normalizeToolSearchPath(path);
|
|
988
|
-
const node = await this.registry.get(canonical).catch(() => null);
|
|
989
|
-
if (node === null || !toolNode(node)) {
|
|
990
|
-
await this.commitMarker(opts.marker, null);
|
|
991
|
-
await this.search.remove(canonical);
|
|
992
|
-
await this.finishMarker(opts.marker);
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
const tools = await this.rawTools(node, opts.tools);
|
|
996
|
-
if (tools === null) {
|
|
997
|
-
await this.search.remove(canonical);
|
|
998
|
-
await this.finishMarker(opts.marker);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
if (tools.length === 0) {
|
|
1002
|
-
await this.commitMarker(opts.marker, null);
|
|
1003
|
-
await this.search.remove(canonical);
|
|
1004
|
-
await this.finishMarker(opts.marker);
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
const root = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
|
|
1008
|
-
if (root.truncated) {
|
|
1009
|
-
await this.finishMarker(opts.marker);
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
const feedback = await this.feedbackText(canonical, opts.feedback);
|
|
1013
|
-
const records = this.indexableRecords(canonical, tools, feedback);
|
|
1014
|
-
if (records === null) {
|
|
1015
|
-
await this.commitMarker(opts.marker, null);
|
|
1016
|
-
await this.search.remove(canonical);
|
|
1017
|
-
await this.finishMarker(opts.marker);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
const digest = records.length === 0 ? null : toolSearchSnapshotDigest(records);
|
|
1021
|
-
await this.commitMarker(opts.marker, digest);
|
|
1022
|
-
await this.search.replace(canonical, tools, {
|
|
1023
|
-
feedback
|
|
1024
|
-
});
|
|
1025
|
-
await this.finishMarker(opts.marker);
|
|
1026
|
-
}
|
|
1027
|
-
/** mutation 热路径使用:canonical 已成功时,派生同步失败只保留 marker。 */
|
|
1028
|
-
async reconcileNodeQuietly(path, opts = {}) {
|
|
1029
|
-
try {
|
|
1030
|
-
await this.reconcileNode(path, opts);
|
|
1031
|
-
} catch {
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
async rebuildAll(markers = [], opts = {}) {
|
|
1035
|
-
const snapshot = opts.canonicalNodes === void 0 ? await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT) : { items: [...opts.canonicalNodes], truncated: false };
|
|
1036
|
-
if (snapshot.truncated) return false;
|
|
1037
|
-
const nodes = snapshot.items.filter(toolNode);
|
|
1038
|
-
const authoritativeEmpty = new Set(opts.authoritativeEmpty ?? []);
|
|
1039
|
-
const keys = nodes.flatMap((node) => [KEY_FEEDBACK + node.path, toolCacheKey(node.path)]);
|
|
1040
|
-
const values = await this.state.getMany(keys);
|
|
1041
|
-
const documents = [];
|
|
1042
|
-
for (const node of nodes) {
|
|
1043
|
-
if (authoritativeEmpty.has(node.path)) continue;
|
|
1044
|
-
let tools;
|
|
1045
|
-
if (node.kind === "http") {
|
|
1046
|
-
tools = httpTools(node);
|
|
1047
|
-
} else {
|
|
1048
|
-
tools = deviceTools(node) ?? cachedTools(values.get(toolCacheKey(node.path)));
|
|
1049
|
-
}
|
|
1050
|
-
if (tools === null) continue;
|
|
1051
|
-
const feedback = selectFeedbackSearchText(
|
|
1052
|
-
parseFeedbackEntries(values.get(KEY_FEEDBACK + node.path))
|
|
1053
|
-
);
|
|
1054
|
-
if (this.indexableRecords(node.path, tools, feedback) === null) continue;
|
|
1055
|
-
documents.push(...documentsFor(node.path, tools, feedback));
|
|
1056
|
-
}
|
|
1057
|
-
await this.search.rebuild(documents);
|
|
1058
|
-
await Promise.all(markers.map(async (marker) => {
|
|
1059
|
-
await this.state.delete(marker.key).catch(() => {
|
|
1060
|
-
});
|
|
1061
|
-
}));
|
|
1062
|
-
return true;
|
|
1063
|
-
}
|
|
1064
|
-
/** 每次搜索前审计 canonical 快照;同快照 rebuild 不 bump revision。 */
|
|
1065
|
-
async ensureReady() {
|
|
1066
|
-
const snapshot = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
|
|
1067
|
-
if (snapshot.truncated) {
|
|
1068
|
-
if (await this.search.initialized()) return;
|
|
1069
|
-
throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
|
|
1070
|
-
}
|
|
1071
|
-
const dirty = await this.dirtyMarkers();
|
|
1072
|
-
await this.rebuildAll(dirty, { canonicalNodes: snapshot.items });
|
|
1073
|
-
}
|
|
1074
|
-
/** 首次 registry/device mutation 前建立 last-known-good,避免并发溢出抢在首次 seed 前。 */
|
|
1075
|
-
async ensureSeeded() {
|
|
1076
|
-
if (await this.search.initialized()) return;
|
|
1077
|
-
if (this.seedPromise === void 0) {
|
|
1078
|
-
this.seedPromise = (async () => {
|
|
1079
|
-
if (!await this.rebuildAll()) {
|
|
1080
|
-
throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
|
|
1081
|
-
}
|
|
1082
|
-
})();
|
|
1083
|
-
}
|
|
1084
|
-
try {
|
|
1085
|
-
await this.seedPromise;
|
|
1086
|
-
} finally {
|
|
1087
|
-
this.seedPromise = void 0;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
async removeSubtreeQuietly(path, marker) {
|
|
1091
|
-
try {
|
|
1092
|
-
const canonical = normalizeToolSearchPath(path);
|
|
1093
|
-
if ((await this.registry.subtree(canonical)).length === 0) {
|
|
1094
|
-
await this.commitMarker(marker, null);
|
|
1095
|
-
await this.search.removePrefix(canonical);
|
|
1096
|
-
await this.finishMarker(marker);
|
|
1097
|
-
} else {
|
|
1098
|
-
await this.rebuildAll(marker === void 0 ? [] : [marker]);
|
|
836
|
+
async function runCursorCleanup(opts) {
|
|
837
|
+
const { state } = opts;
|
|
838
|
+
if (state.compareAndSwap === void 0) {
|
|
839
|
+
throw new TBError("unavailable", "cursor cleanup requires StateStore.compareAndSwap");
|
|
840
|
+
}
|
|
841
|
+
const raw = await state.get(opts.progressKey);
|
|
842
|
+
let progress = raw === null ? null : opts.parseProgress(raw);
|
|
843
|
+
let cursor = progress?.cursor;
|
|
844
|
+
for (let pageNumber = 0; pageNumber < opts.maxPages; pageNumber++) {
|
|
845
|
+
const nextCursor = await opts.runPage(cursor, pageNumber);
|
|
846
|
+
if (nextCursor === void 0) {
|
|
847
|
+
if (progress !== null) {
|
|
848
|
+
await state.compareAndSwap(opts.progressKey, progress.revision, null);
|
|
1099
849
|
}
|
|
1100
|
-
|
|
850
|
+
return void 0;
|
|
1101
851
|
}
|
|
852
|
+
const next = {
|
|
853
|
+
cursor: nextCursor,
|
|
854
|
+
revision: (progress?.revision ?? 0) + 1
|
|
855
|
+
};
|
|
856
|
+
const advanced = await state.compareAndSwap(
|
|
857
|
+
opts.progressKey,
|
|
858
|
+
progress?.revision ?? null,
|
|
859
|
+
opts.encodeProgress(next)
|
|
860
|
+
);
|
|
861
|
+
if (!advanced) return void 0;
|
|
862
|
+
progress = next;
|
|
863
|
+
cursor = next.cursor;
|
|
1102
864
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
// ../app/src/deps.ts
|
|
1106
|
-
var TOOL_CACHE_TTL_DEFAULT = 300;
|
|
865
|
+
return cursor;
|
|
866
|
+
}
|
|
1107
867
|
|
|
1108
868
|
// ../app/src/storeRefToken.ts
|
|
1109
869
|
async function hmacKey(secret) {
|
|
@@ -1162,13 +922,6 @@ var STORE_READ_TTL_SEC_DEFAULT = 15 * 60;
|
|
|
1162
922
|
var STORE_CALL_MAX_OBJECTS_DEFAULT = 4;
|
|
1163
923
|
var STORE_CALL_MAX_OBJECT_BYTES_DEFAULT = 256 * 1024 * 1024;
|
|
1164
924
|
var STORE_CALL_MAX_BYTES_DEFAULT = 512 * 1024 * 1024;
|
|
1165
|
-
function positiveInt(value, fallback, field) {
|
|
1166
|
-
const actual = value ?? fallback;
|
|
1167
|
-
if (!Number.isSafeInteger(actual) || actual < 1) {
|
|
1168
|
-
throw new TBError("invalid_argument", `${field} must be a positive integer`);
|
|
1169
|
-
}
|
|
1170
|
-
return actual;
|
|
1171
|
-
}
|
|
1172
925
|
function randomSecret() {
|
|
1173
926
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
1174
927
|
let out = "";
|
|
@@ -1501,44 +1254,28 @@ async function cleanupDefaultStore(deps, opts = {}) {
|
|
|
1501
1254
|
const store = await defaultStoreRuntime(deps);
|
|
1502
1255
|
const maxPages = Math.min(positiveInt(opts.maxPages, 8, "maxPages"), 64);
|
|
1503
1256
|
const limit = opts.limit === void 0 ? void 0 : positiveInt(opts.limit, 200, "limit");
|
|
1504
|
-
const rawProgress = await deps.state.get(KEY_STORE_CLEANUP_PROGRESS);
|
|
1505
|
-
let progress = rawProgress === null ? null : cleanupProgress(rawProgress);
|
|
1506
|
-
let cursors = progress?.cursors;
|
|
1507
1257
|
const aggregate = emptyCleanupResult();
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
return
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
revision: (progress?.revision ?? 0) + 1
|
|
1530
|
-
};
|
|
1531
|
-
const advanced = await deps.state.compareAndSwap(
|
|
1532
|
-
KEY_STORE_CLEANUP_PROGRESS,
|
|
1533
|
-
progress?.revision ?? null,
|
|
1534
|
-
next
|
|
1535
|
-
);
|
|
1536
|
-
if (!advanced) {
|
|
1537
|
-
return aggregate;
|
|
1538
|
-
}
|
|
1539
|
-
progress = next;
|
|
1540
|
-
cursors = next.cursors;
|
|
1541
|
-
}
|
|
1258
|
+
const cursors = await runCursorCleanup({
|
|
1259
|
+
encodeProgress: (progress) => ({ cursors: progress.cursor, revision: progress.revision }),
|
|
1260
|
+
maxPages,
|
|
1261
|
+
parseProgress: (value) => {
|
|
1262
|
+
const progress = cleanupProgress(value);
|
|
1263
|
+
return { cursor: progress.cursors, revision: progress.revision };
|
|
1264
|
+
},
|
|
1265
|
+
progressKey: KEY_STORE_CLEANUP_PROGRESS,
|
|
1266
|
+
runPage: async (cursor, pageNumber) => {
|
|
1267
|
+
const page = await store.service.cleanup({
|
|
1268
|
+
...limit === void 0 ? {} : { limit },
|
|
1269
|
+
...cursor === void 0 ? {} : { cursors: cursor },
|
|
1270
|
+
// staging 不使用权威状态 cursor;同一 host tick 只运行一次,避免
|
|
1271
|
+
// maxPages 内重复扫描同一 driver maintenance 范围。
|
|
1272
|
+
runDriverMaintenance: pageNumber === 0
|
|
1273
|
+
});
|
|
1274
|
+
addCleanupResult(aggregate, page);
|
|
1275
|
+
return page.cursors;
|
|
1276
|
+
},
|
|
1277
|
+
state: deps.state
|
|
1278
|
+
});
|
|
1542
1279
|
return { ...aggregate, ...cursors === void 0 ? {} : { cursors } };
|
|
1543
1280
|
}
|
|
1544
1281
|
|
|
@@ -1552,12 +1289,12 @@ function requireDevice(deps) {
|
|
|
1552
1289
|
}
|
|
1553
1290
|
return deps.device;
|
|
1554
1291
|
}
|
|
1555
|
-
async function
|
|
1292
|
+
async function attemptDevice(deps, deviceId, req) {
|
|
1556
1293
|
const id = crypto.randomUUID();
|
|
1557
1294
|
const upload = req.context === void 0 ? null : await issueDeviceCallUpload(deps, deviceId, id, req.context);
|
|
1558
|
-
let
|
|
1295
|
+
let attempt2;
|
|
1559
1296
|
try {
|
|
1560
|
-
|
|
1297
|
+
attempt2 = await requireDevice(deps).invoke(deviceId, {
|
|
1561
1298
|
id,
|
|
1562
1299
|
path: req.path,
|
|
1563
1300
|
arguments: req.arguments,
|
|
@@ -1567,11 +1304,15 @@ async function invokeDevice(deps, deviceId, req) {
|
|
|
1567
1304
|
await upload?.revoke().catch(() => {
|
|
1568
1305
|
});
|
|
1569
1306
|
}
|
|
1570
|
-
if (!
|
|
1307
|
+
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) {
|
|
1571
1308
|
throw new TBError("unavailable", "device session returned invalid result");
|
|
1572
1309
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1310
|
+
return attempt2;
|
|
1311
|
+
}
|
|
1312
|
+
async function invokeDevice(deps, deviceId, req) {
|
|
1313
|
+
const attempt2 = await attemptDevice(deps, deviceId, req);
|
|
1314
|
+
if (attempt2.result.ok) return attempt2.result.value;
|
|
1315
|
+
throw tbErrorFromBody(attempt2.result.error);
|
|
1575
1316
|
}
|
|
1576
1317
|
function deviceCallContextFrom(ctx) {
|
|
1577
1318
|
const now = Date.now();
|
|
@@ -19794,8 +19535,6 @@ function createPluginToolProvider(opts) {
|
|
|
19794
19535
|
|
|
19795
19536
|
// ../app/src/providerOAuth.ts
|
|
19796
19537
|
var KEY_TOKENS = "puoauth:token:";
|
|
19797
|
-
var STATE_TTL_SEC2 = 600;
|
|
19798
|
-
var PROVIDER_OAUTH_CALLBACK_PATH = "/~oauth/callback";
|
|
19799
19538
|
async function invalidateProviderOAuth(store, nodePath) {
|
|
19800
19539
|
await store.delete(KEY_TOKENS + nodePath);
|
|
19801
19540
|
}
|
|
@@ -19870,15 +19609,10 @@ async function requestTokens(opts) {
|
|
|
19870
19609
|
throw err;
|
|
19871
19610
|
}
|
|
19872
19611
|
}
|
|
19873
|
-
function base64url(bytes) {
|
|
19874
|
-
let binary = "";
|
|
19875
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
19876
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
19877
|
-
}
|
|
19878
19612
|
async function createPkce() {
|
|
19879
|
-
const verifier =
|
|
19613
|
+
const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
19880
19614
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
19881
|
-
return { verifier, challenge:
|
|
19615
|
+
return { verifier, challenge: base64urlEncode(new Uint8Array(digest)) };
|
|
19882
19616
|
}
|
|
19883
19617
|
function requireProviderOAuthFetch(fetcher) {
|
|
19884
19618
|
if (fetcher === void 0) {
|
|
@@ -19914,7 +19648,7 @@ async function startProviderAuthorization(opts) {
|
|
|
19914
19648
|
{
|
|
19915
19649
|
p: opts.nodePath,
|
|
19916
19650
|
v: pkce?.verifier ?? "",
|
|
19917
|
-
exp: Math.floor(opts.now.getTime() / 1e3) +
|
|
19651
|
+
exp: Math.floor(opts.now.getTime() / 1e3) + STATE_TTL_SEC
|
|
19918
19652
|
},
|
|
19919
19653
|
opts.encryptionKey
|
|
19920
19654
|
);
|
|
@@ -19923,7 +19657,7 @@ async function startProviderAuthorization(opts) {
|
|
|
19923
19657
|
authorizationUrl: buildAuthorizationUrl({
|
|
19924
19658
|
config: opts.config,
|
|
19925
19659
|
clientId: client.clientId,
|
|
19926
|
-
redirectUri: opts.origin +
|
|
19660
|
+
redirectUri: opts.origin + OAUTH_CALLBACK_PATH,
|
|
19927
19661
|
state,
|
|
19928
19662
|
...pkce === void 0 ? {} : { codeChallenge: pkce.challenge }
|
|
19929
19663
|
})
|
|
@@ -19936,7 +19670,7 @@ async function finishProviderAuthorization(opts) {
|
|
|
19936
19670
|
config: opts.config,
|
|
19937
19671
|
fetcher,
|
|
19938
19672
|
now: opts.now,
|
|
19939
|
-
grant: { code: opts.code, redirectUri: opts.origin +
|
|
19673
|
+
grant: { code: opts.code, redirectUri: opts.origin + OAUTH_CALLBACK_PATH },
|
|
19940
19674
|
...opts.codeVerifier === "" ? {} : { codeVerifier: opts.codeVerifier },
|
|
19941
19675
|
...client
|
|
19942
19676
|
});
|
|
@@ -19966,21 +19700,61 @@ async function resolveProviderAccessToken(opts) {
|
|
|
19966
19700
|
return refreshed.accessToken;
|
|
19967
19701
|
}
|
|
19968
19702
|
|
|
19703
|
+
// ../app/src/providers/toolCache.ts
|
|
19704
|
+
var KEY_PREFIX = "toolcache:";
|
|
19705
|
+
function toolCacheKey(path) {
|
|
19706
|
+
return `${KEY_PREFIX}${path}`;
|
|
19707
|
+
}
|
|
19708
|
+
function isCached(v) {
|
|
19709
|
+
return typeof v === "object" && v !== null && Array.isArray(v.tools) && typeof v.fetchedAt === "string";
|
|
19710
|
+
}
|
|
19711
|
+
function isFresh(fetchedAt, ttl, now) {
|
|
19712
|
+
const age = (Date.parse(now) - Date.parse(fetchedAt)) / 1e3;
|
|
19713
|
+
return Number.isFinite(age) && age >= 0 && age < ttl;
|
|
19714
|
+
}
|
|
19715
|
+
async function invalidateToolCache(store, path) {
|
|
19716
|
+
await store.delete(toolCacheKey(path));
|
|
19717
|
+
}
|
|
19718
|
+
function cachedTools(value) {
|
|
19719
|
+
return isCached(value) ? value.tools : null;
|
|
19720
|
+
}
|
|
19721
|
+
async function peekToolCache(store, path) {
|
|
19722
|
+
return cachedTools(await store.get(toolCacheKey(path)));
|
|
19723
|
+
}
|
|
19724
|
+
async function getTools(store, path, fetchList, opts) {
|
|
19725
|
+
if (!opts.refresh) {
|
|
19726
|
+
const raw = await store.get(toolCacheKey(path));
|
|
19727
|
+
if (isCached(raw) && isFresh(raw.fetchedAt, opts.ttl, opts.now)) {
|
|
19728
|
+
return raw.tools;
|
|
19729
|
+
}
|
|
19730
|
+
}
|
|
19731
|
+
await opts.beforeFresh?.();
|
|
19732
|
+
try {
|
|
19733
|
+
const tools = await fetchList();
|
|
19734
|
+
const record2 = { tools, fetchedAt: opts.now };
|
|
19735
|
+
await store.put(toolCacheKey(path), record2);
|
|
19736
|
+
await opts.onFresh?.(tools);
|
|
19737
|
+
return tools;
|
|
19738
|
+
} catch (error) {
|
|
19739
|
+
await opts.onFreshError?.();
|
|
19740
|
+
throw error;
|
|
19741
|
+
}
|
|
19742
|
+
}
|
|
19743
|
+
|
|
19969
19744
|
// ../app/src/toolNodes.ts
|
|
19970
|
-
function upstreamTools(node, provider, deps, refresh, now) {
|
|
19745
|
+
function upstreamTools(node, provider, deps, refresh, now, searchSync) {
|
|
19971
19746
|
if (node.kind === "mcp" || node.kind === "tool") {
|
|
19972
|
-
const sync = isMutableSearchIndex(deps.search) ? new SearchSynchronizer(deps.state, deps.search) : void 0;
|
|
19973
19747
|
let marker;
|
|
19974
19748
|
return getTools(deps.state, node.path, () => provider.list(), {
|
|
19975
19749
|
refresh,
|
|
19976
19750
|
ttl: deps.toolCacheTtlSec ?? TOOL_CACHE_TTL_DEFAULT,
|
|
19977
19751
|
now,
|
|
19978
|
-
...
|
|
19752
|
+
...searchSync === void 0 ? {} : {
|
|
19979
19753
|
beforeFresh: async () => {
|
|
19980
|
-
marker = await
|
|
19754
|
+
marker = await searchSync.markNode(node.path);
|
|
19981
19755
|
},
|
|
19982
|
-
onFreshError: async () => await
|
|
19983
|
-
onFresh: async (tools) => await
|
|
19756
|
+
onFreshError: async () => await searchSync.abort(marker),
|
|
19757
|
+
onFresh: async (tools) => await searchSync.reconcileNodeQuietly(node.path, { marker, tools })
|
|
19984
19758
|
}
|
|
19985
19759
|
});
|
|
19986
19760
|
}
|
|
@@ -20115,11 +19889,11 @@ async function providerFor(node, ctx, deps) {
|
|
|
20115
19889
|
}
|
|
20116
19890
|
throw TBError.unimplemented(`kind '${node.kind}' has no tool provider`);
|
|
20117
19891
|
}
|
|
20118
|
-
async function refreshDynamicSearchNode(node, ctx, deps) {
|
|
19892
|
+
async function refreshDynamicSearchNode(node, ctx, deps, searchSync) {
|
|
20119
19893
|
if (node.kind !== "mcp" && node.kind !== "tool" || deviceToolMarker(node) !== null) return false;
|
|
20120
19894
|
try {
|
|
20121
19895
|
const provider = await providerFor(node, ctx, deps);
|
|
20122
|
-
await upstreamTools(node, provider, deps, true, (/* @__PURE__ */ new Date()).toISOString());
|
|
19896
|
+
await upstreamTools(node, provider, deps, true, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
|
|
20123
19897
|
return true;
|
|
20124
19898
|
} catch {
|
|
20125
19899
|
return false;
|
|
@@ -20134,7 +19908,7 @@ async function refreshDynamicToolCache(node, ctx, deps) {
|
|
|
20134
19908
|
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
20135
19909
|
});
|
|
20136
19910
|
}
|
|
20137
|
-
async function toolHelpModelFor(c, ctx, registry, path, deps) {
|
|
19911
|
+
async function toolHelpModelFor(c, ctx, registry, path, deps, searchSync) {
|
|
20138
19912
|
let resolved;
|
|
20139
19913
|
try {
|
|
20140
19914
|
resolved = await registry.resolve(path);
|
|
@@ -20155,7 +19929,7 @@ async function toolHelpModelFor(c, ctx, registry, path, deps) {
|
|
|
20155
19929
|
}
|
|
20156
19930
|
const provider = await providerFor(node, ctx, deps);
|
|
20157
19931
|
const refresh = c.req.query("refresh") === "1";
|
|
20158
|
-
const raw = await upstreamTools(node, provider, deps, refresh, (/* @__PURE__ */ new Date()).toISOString());
|
|
19932
|
+
const raw = await upstreamTools(node, provider, deps, refresh, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
|
|
20159
19933
|
const { exposed } = virtualizeTools(node.virtualize, raw);
|
|
20160
19934
|
const tool = exposed.find((t) => t.name === rest);
|
|
20161
19935
|
if (tool === void 0) return null;
|
|
@@ -20308,9 +20082,6 @@ async function verifyRefToken(token, secret) {
|
|
|
20308
20082
|
function localContext(deps, node) {
|
|
20309
20083
|
return deps.locals?.context?.(node.path) ?? null;
|
|
20310
20084
|
}
|
|
20311
|
-
function localCapabilities(provider) {
|
|
20312
|
-
return contextCapabilitiesOf(provider);
|
|
20313
|
-
}
|
|
20314
20085
|
function parseS3Credentials(raw, refName) {
|
|
20315
20086
|
try {
|
|
20316
20087
|
const v = JSON.parse(raw);
|
|
@@ -20387,13 +20158,8 @@ async function createContextUploadGrant(node, cfg, deps, input) {
|
|
|
20387
20158
|
uploadGrantTtlSec
|
|
20388
20159
|
}, input);
|
|
20389
20160
|
}
|
|
20390
|
-
|
|
20391
|
-
const
|
|
20392
|
-
const opts = {
|
|
20393
|
-
nsPath: node.path,
|
|
20394
|
-
keyPrefix: contextKeyPrefix(cfg, node.path),
|
|
20395
|
-
readOnly: cfg.readOnly ?? false
|
|
20396
|
-
};
|
|
20161
|
+
function objectProviderOpts(node, keyPrefix, readOnly, deps, requestUrl) {
|
|
20162
|
+
const opts = { nsPath: node.path, keyPrefix, readOnly };
|
|
20397
20163
|
if (deps.refThresholdBytes !== void 0) opts.refThresholdBytes = deps.refThresholdBytes;
|
|
20398
20164
|
if (deps.refTtlSec !== void 0) opts.presignTtlSec = deps.refTtlSec;
|
|
20399
20165
|
const encKey = deps.encryptionKey;
|
|
@@ -20405,7 +20171,17 @@ async function contextProviderFor(node, cfg, deps, requestUrl) {
|
|
|
20405
20171
|
return `${origin}/~ref/${await signRefToken({ p: node.path, k: key, exp }, encKey)}`;
|
|
20406
20172
|
};
|
|
20407
20173
|
}
|
|
20408
|
-
return
|
|
20174
|
+
return opts;
|
|
20175
|
+
}
|
|
20176
|
+
async function contextProviderFor(node, cfg, deps, requestUrl) {
|
|
20177
|
+
const objects = await contextObjectStoreFor(cfg, deps);
|
|
20178
|
+
return createObjectContextProvider(objects, objectProviderOpts(
|
|
20179
|
+
node,
|
|
20180
|
+
contextKeyPrefix(cfg, node.path),
|
|
20181
|
+
cfg.readOnly ?? false,
|
|
20182
|
+
deps,
|
|
20183
|
+
requestUrl
|
|
20184
|
+
));
|
|
20409
20185
|
}
|
|
20410
20186
|
function skillhubKeyPrefix(cfg, nodePath) {
|
|
20411
20187
|
const prefix = cfg.providerConfig?.prefix;
|
|
@@ -20414,29 +20190,13 @@ function skillhubKeyPrefix(cfg, nodePath) {
|
|
|
20414
20190
|
}
|
|
20415
20191
|
async function skillhubProviderFor(node, cfg, deps, requestUrl) {
|
|
20416
20192
|
const objects = await contextObjectStoreFor(cfg, deps);
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
const encKey = deps.encryptionKey;
|
|
20425
|
-
if (encKey !== void 0) {
|
|
20426
|
-
const origin = new URL(requestUrl).origin;
|
|
20427
|
-
const relayTtlSec = deps.refTtlSec ?? PRESIGN_TTL_SEC_DEFAULT;
|
|
20428
|
-
opts.relayRefUrl = async (key) => {
|
|
20429
|
-
const exp = Math.floor(Date.now() / 1e3) + relayTtlSec;
|
|
20430
|
-
return `${origin}/~ref/${await signRefToken({ p: node.path, k: key, exp }, encKey)}`;
|
|
20431
|
-
};
|
|
20432
|
-
}
|
|
20433
|
-
return createSkillhubProvider(objects, opts);
|
|
20434
|
-
}
|
|
20435
|
-
async function dispatchSkillhubCmd2(provider, command, args) {
|
|
20436
|
-
return await dispatchSkillhubCmd(provider, command, args);
|
|
20437
|
-
}
|
|
20438
|
-
async function dispatchContextCmd2(provider, command, args) {
|
|
20439
|
-
return await dispatchContextCmd(provider, command, args);
|
|
20193
|
+
return createSkillhubProvider(objects, objectProviderOpts(
|
|
20194
|
+
node,
|
|
20195
|
+
skillhubKeyPrefix(cfg, node.path),
|
|
20196
|
+
cfg.readOnly ?? false,
|
|
20197
|
+
deps,
|
|
20198
|
+
requestUrl
|
|
20199
|
+
));
|
|
20440
20200
|
}
|
|
20441
20201
|
async function assertContextAlive(node, cfg, registry) {
|
|
20442
20202
|
if (!isContextExpired(node.createdAt, cfg.ttl, Date.now())) return;
|
|
@@ -20507,6 +20267,257 @@ async function assertSkillhubConfig(config, deps) {
|
|
|
20507
20267
|
}
|
|
20508
20268
|
}
|
|
20509
20269
|
|
|
20270
|
+
// ../app/src/search/synchronizer.ts
|
|
20271
|
+
var DIRTY_NODE_PREFIX = "searchdirty:node:";
|
|
20272
|
+
var DIRTY_SUBTREE_PREFIX = "searchdirty:subtree:";
|
|
20273
|
+
function isMutableSearchIndex(value) {
|
|
20274
|
+
if (typeof value !== "object" || value === null) return false;
|
|
20275
|
+
const index = value;
|
|
20276
|
+
return typeof index.replace === "function" && typeof index.remove === "function" && typeof index.removePrefix === "function" && typeof index.rebuild === "function" && typeof index.initialized === "function";
|
|
20277
|
+
}
|
|
20278
|
+
function toolNode(node) {
|
|
20279
|
+
return (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config?.kind === node.kind;
|
|
20280
|
+
}
|
|
20281
|
+
function httpTools(node) {
|
|
20282
|
+
if (node.config?.kind !== "http") return null;
|
|
20283
|
+
return node.config.tools.map((tool) => {
|
|
20284
|
+
const spec = {
|
|
20285
|
+
description: tool.description,
|
|
20286
|
+
effect: effectFor(tool),
|
|
20287
|
+
name: tool.name
|
|
20288
|
+
};
|
|
20289
|
+
if (tool.inputSchema !== void 0) spec.inputSchema = tool.inputSchema;
|
|
20290
|
+
return spec;
|
|
20291
|
+
});
|
|
20292
|
+
}
|
|
20293
|
+
function deviceTools(node) {
|
|
20294
|
+
if (node.config?.kind !== "tool") return null;
|
|
20295
|
+
const cmds = node.config.providerConfig?.cmds;
|
|
20296
|
+
if (!Array.isArray(cmds)) return null;
|
|
20297
|
+
return cmds;
|
|
20298
|
+
}
|
|
20299
|
+
async function canonicalSearchTools(state, nodes) {
|
|
20300
|
+
const unique = new Map(nodes.map((node) => [node.path, node]));
|
|
20301
|
+
const dynamicPaths = [...unique.values()].filter((node) => node.kind !== "http" && deviceTools(node) === null).map((node) => node.path);
|
|
20302
|
+
const cached = dynamicPaths.length === 0 ? /* @__PURE__ */ new Map() : await state.getMany(dynamicPaths.map(toolCacheKey));
|
|
20303
|
+
const result = /* @__PURE__ */ new Map();
|
|
20304
|
+
for (const node of unique.values()) {
|
|
20305
|
+
const tools = node.kind === "http" ? httpTools(node) : deviceTools(node) ?? cachedTools(cached.get(toolCacheKey(node.path)));
|
|
20306
|
+
if (tools !== null) result.set(node.path, tools);
|
|
20307
|
+
}
|
|
20308
|
+
return result;
|
|
20309
|
+
}
|
|
20310
|
+
function documentsFor(path, tools, feedback) {
|
|
20311
|
+
return tools.map((tool) => ({ path, tool, ...feedback === "" ? {} : { feedback } }));
|
|
20312
|
+
}
|
|
20313
|
+
var SearchSynchronizer = class {
|
|
20314
|
+
constructor(state, search) {
|
|
20315
|
+
this.state = state;
|
|
20316
|
+
this.search = search;
|
|
20317
|
+
this.registry = new NodeRegistryStore(state);
|
|
20318
|
+
}
|
|
20319
|
+
state;
|
|
20320
|
+
search;
|
|
20321
|
+
registry;
|
|
20322
|
+
seedPromise;
|
|
20323
|
+
async mark(path, kind) {
|
|
20324
|
+
const canonical = normalizeToolSearchPath(path);
|
|
20325
|
+
const marker = {
|
|
20326
|
+
createdAt: Date.now(),
|
|
20327
|
+
// Marker 只是审计提示,正确性由每次 canonical audit 保证。每类固定一个 key,
|
|
20328
|
+
// 避免 overflow/device hello 用 UUID marker 把 KV 与扫描成本无界放大。
|
|
20329
|
+
key: `${kind === "node" ? DIRTY_NODE_PREFIX : DIRTY_SUBTREE_PREFIX}pending`,
|
|
20330
|
+
kind,
|
|
20331
|
+
path: canonical
|
|
20332
|
+
};
|
|
20333
|
+
await this.state.put(marker.key, marker);
|
|
20334
|
+
return marker;
|
|
20335
|
+
}
|
|
20336
|
+
async markNode(path) {
|
|
20337
|
+
return await this.mark(path, "node");
|
|
20338
|
+
}
|
|
20339
|
+
async markSubtree(path) {
|
|
20340
|
+
return await this.mark(path, "subtree");
|
|
20341
|
+
}
|
|
20342
|
+
async abort(marker) {
|
|
20343
|
+
if (marker !== void 0 && marker.expectedDigest === void 0) {
|
|
20344
|
+
await this.state.delete(marker.key);
|
|
20345
|
+
}
|
|
20346
|
+
}
|
|
20347
|
+
async dirtyMarkers() {
|
|
20348
|
+
const markers = [];
|
|
20349
|
+
for (const prefix of [DIRTY_NODE_PREFIX, DIRTY_SUBTREE_PREFIX]) {
|
|
20350
|
+
let cursor;
|
|
20351
|
+
do {
|
|
20352
|
+
const page = await this.state.list(prefix, {
|
|
20353
|
+
limit: 200,
|
|
20354
|
+
...cursor === void 0 ? {} : { cursor }
|
|
20355
|
+
});
|
|
20356
|
+
for (const item of page.items) {
|
|
20357
|
+
const marker = item.value;
|
|
20358
|
+
if (typeof marker.createdAt === "number" && marker.key === item.key && (marker.kind === "node" || marker.kind === "subtree") && typeof marker.path === "string") {
|
|
20359
|
+
markers.push(marker);
|
|
20360
|
+
}
|
|
20361
|
+
}
|
|
20362
|
+
cursor = page.cursor;
|
|
20363
|
+
} while (cursor !== void 0);
|
|
20364
|
+
}
|
|
20365
|
+
return markers;
|
|
20366
|
+
}
|
|
20367
|
+
async commitMarker(marker, expectedDigest) {
|
|
20368
|
+
if (marker === void 0) return;
|
|
20369
|
+
const committed = {
|
|
20370
|
+
...marker,
|
|
20371
|
+
committedAt: Date.now(),
|
|
20372
|
+
expectedDigest
|
|
20373
|
+
};
|
|
20374
|
+
await this.state.put(marker.key, committed);
|
|
20375
|
+
Object.assign(marker, committed);
|
|
20376
|
+
}
|
|
20377
|
+
async finishMarker(marker) {
|
|
20378
|
+
if (marker !== void 0) await this.state.delete(marker.key);
|
|
20379
|
+
}
|
|
20380
|
+
async feedbackText(path, snapshot) {
|
|
20381
|
+
if (snapshot !== void 0) return selectFeedbackSearchText(snapshot);
|
|
20382
|
+
return selectFeedbackSearchText(parseFeedbackEntries(await this.state.get(KEY_FEEDBACK + path)));
|
|
20383
|
+
}
|
|
20384
|
+
async rawTools(node, override) {
|
|
20385
|
+
if (override !== void 0) return [...override];
|
|
20386
|
+
if (node.kind === "http") return httpTools(node);
|
|
20387
|
+
const device = deviceTools(node);
|
|
20388
|
+
if (device !== null) return device;
|
|
20389
|
+
if (node.kind === "mcp" || node.kind === "tool") {
|
|
20390
|
+
return await peekToolCache(this.state, node.path);
|
|
20391
|
+
}
|
|
20392
|
+
return null;
|
|
20393
|
+
}
|
|
20394
|
+
indexableRecords(path, tools, feedback) {
|
|
20395
|
+
try {
|
|
20396
|
+
return serializeToolSearchSnapshot(path, tools, feedback);
|
|
20397
|
+
} catch (error) {
|
|
20398
|
+
if (isTBError(error)) return null;
|
|
20399
|
+
throw error;
|
|
20400
|
+
}
|
|
20401
|
+
}
|
|
20402
|
+
async reconcileNode(path, opts = {}) {
|
|
20403
|
+
const canonical = normalizeToolSearchPath(path);
|
|
20404
|
+
const node = await this.registry.get(canonical).catch(() => null);
|
|
20405
|
+
if (node === null || !toolNode(node)) {
|
|
20406
|
+
await this.commitMarker(opts.marker, null);
|
|
20407
|
+
await this.search.remove(canonical);
|
|
20408
|
+
await this.finishMarker(opts.marker);
|
|
20409
|
+
return;
|
|
20410
|
+
}
|
|
20411
|
+
const tools = await this.rawTools(node, opts.tools);
|
|
20412
|
+
if (tools === null) {
|
|
20413
|
+
await this.search.remove(canonical);
|
|
20414
|
+
await this.finishMarker(opts.marker);
|
|
20415
|
+
return;
|
|
20416
|
+
}
|
|
20417
|
+
if (tools.length === 0) {
|
|
20418
|
+
await this.commitMarker(opts.marker, null);
|
|
20419
|
+
await this.search.remove(canonical);
|
|
20420
|
+
await this.finishMarker(opts.marker);
|
|
20421
|
+
return;
|
|
20422
|
+
}
|
|
20423
|
+
const root = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
|
|
20424
|
+
if (root.truncated) {
|
|
20425
|
+
await this.finishMarker(opts.marker);
|
|
20426
|
+
return;
|
|
20427
|
+
}
|
|
20428
|
+
const feedback = await this.feedbackText(canonical, opts.feedback);
|
|
20429
|
+
const records = this.indexableRecords(canonical, tools, feedback);
|
|
20430
|
+
if (records === null) {
|
|
20431
|
+
await this.commitMarker(opts.marker, null);
|
|
20432
|
+
await this.search.remove(canonical);
|
|
20433
|
+
await this.finishMarker(opts.marker);
|
|
20434
|
+
return;
|
|
20435
|
+
}
|
|
20436
|
+
const digest = records.length === 0 ? null : toolSearchSnapshotDigest(records);
|
|
20437
|
+
await this.commitMarker(opts.marker, digest);
|
|
20438
|
+
await this.search.replace(canonical, tools, {
|
|
20439
|
+
feedback
|
|
20440
|
+
});
|
|
20441
|
+
await this.finishMarker(opts.marker);
|
|
20442
|
+
}
|
|
20443
|
+
/** mutation 热路径使用:canonical 已成功时,派生同步失败只保留 marker。 */
|
|
20444
|
+
async reconcileNodeQuietly(path, opts = {}) {
|
|
20445
|
+
try {
|
|
20446
|
+
await this.reconcileNode(path, opts);
|
|
20447
|
+
} catch {
|
|
20448
|
+
}
|
|
20449
|
+
}
|
|
20450
|
+
async rebuildAll(markers = [], opts = {}) {
|
|
20451
|
+
const snapshot = opts.canonicalNodes === void 0 ? await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT) : { items: [...opts.canonicalNodes], truncated: false };
|
|
20452
|
+
if (snapshot.truncated) return false;
|
|
20453
|
+
const nodes = snapshot.items.filter(toolNode);
|
|
20454
|
+
const authoritativeEmpty = new Set(opts.authoritativeEmpty ?? []);
|
|
20455
|
+
const keys = nodes.flatMap((node) => [KEY_FEEDBACK + node.path, toolCacheKey(node.path)]);
|
|
20456
|
+
const values = await this.state.getMany(keys);
|
|
20457
|
+
const documents = [];
|
|
20458
|
+
for (const node of nodes) {
|
|
20459
|
+
if (authoritativeEmpty.has(node.path)) continue;
|
|
20460
|
+
let tools;
|
|
20461
|
+
if (node.kind === "http") {
|
|
20462
|
+
tools = httpTools(node);
|
|
20463
|
+
} else {
|
|
20464
|
+
tools = deviceTools(node) ?? cachedTools(values.get(toolCacheKey(node.path)));
|
|
20465
|
+
}
|
|
20466
|
+
if (tools === null) continue;
|
|
20467
|
+
const feedback = selectFeedbackSearchText(
|
|
20468
|
+
parseFeedbackEntries(values.get(KEY_FEEDBACK + node.path))
|
|
20469
|
+
);
|
|
20470
|
+
if (this.indexableRecords(node.path, tools, feedback) === null) continue;
|
|
20471
|
+
documents.push(...documentsFor(node.path, tools, feedback));
|
|
20472
|
+
}
|
|
20473
|
+
await this.search.rebuild(documents);
|
|
20474
|
+
await Promise.all(markers.map(async (marker) => {
|
|
20475
|
+
await this.state.delete(marker.key).catch(() => {
|
|
20476
|
+
});
|
|
20477
|
+
}));
|
|
20478
|
+
return true;
|
|
20479
|
+
}
|
|
20480
|
+
/** 每次搜索前审计 canonical 快照;同快照 rebuild 不 bump revision。 */
|
|
20481
|
+
async ensureReady() {
|
|
20482
|
+
const snapshot = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
|
|
20483
|
+
if (snapshot.truncated) {
|
|
20484
|
+
if (await this.search.initialized()) return;
|
|
20485
|
+
throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
|
|
20486
|
+
}
|
|
20487
|
+
const dirty = await this.dirtyMarkers();
|
|
20488
|
+
await this.rebuildAll(dirty, { canonicalNodes: snapshot.items });
|
|
20489
|
+
}
|
|
20490
|
+
/** 首次 registry/device mutation 前建立 last-known-good,避免并发溢出抢在首次 seed 前。 */
|
|
20491
|
+
async ensureSeeded() {
|
|
20492
|
+
if (await this.search.initialized()) return;
|
|
20493
|
+
if (this.seedPromise === void 0) {
|
|
20494
|
+
this.seedPromise = (async () => {
|
|
20495
|
+
if (!await this.rebuildAll()) {
|
|
20496
|
+
throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
|
|
20497
|
+
}
|
|
20498
|
+
})();
|
|
20499
|
+
}
|
|
20500
|
+
try {
|
|
20501
|
+
await this.seedPromise;
|
|
20502
|
+
} finally {
|
|
20503
|
+
this.seedPromise = void 0;
|
|
20504
|
+
}
|
|
20505
|
+
}
|
|
20506
|
+
async removeSubtreeQuietly(path, marker) {
|
|
20507
|
+
try {
|
|
20508
|
+
const canonical = normalizeToolSearchPath(path);
|
|
20509
|
+
if ((await this.registry.subtree(canonical)).length === 0) {
|
|
20510
|
+
await this.commitMarker(marker, null);
|
|
20511
|
+
await this.search.removePrefix(canonical);
|
|
20512
|
+
await this.finishMarker(marker);
|
|
20513
|
+
} else {
|
|
20514
|
+
await this.rebuildAll(marker === void 0 ? [] : [marker]);
|
|
20515
|
+
}
|
|
20516
|
+
} catch {
|
|
20517
|
+
}
|
|
20518
|
+
}
|
|
20519
|
+
};
|
|
20520
|
+
|
|
20510
20521
|
// ../app/src/deviceHello.ts
|
|
20511
20522
|
function assertDeviceId(deviceId) {
|
|
20512
20523
|
if (!/^[A-Za-z0-9._-]+$/.test(deviceId)) {
|
|
@@ -20546,6 +20557,7 @@ function customNodeInput(mountPath, deviceId, raw) {
|
|
|
20546
20557
|
return input;
|
|
20547
20558
|
}
|
|
20548
20559
|
function nodesForHello(mountPath, deviceId, expose) {
|
|
20560
|
+
const rawTargets = [mountPath];
|
|
20549
20561
|
const nodes = [
|
|
20550
20562
|
{
|
|
20551
20563
|
path: mountPath,
|
|
@@ -20554,6 +20566,7 @@ function nodesForHello(mountPath, deviceId, expose) {
|
|
|
20554
20566
|
}
|
|
20555
20567
|
];
|
|
20556
20568
|
if (expose.shell !== void 0) {
|
|
20569
|
+
rawTargets.push(joinTreePath(mountPath, "shell"));
|
|
20557
20570
|
nodes.push({
|
|
20558
20571
|
path: joinTreePath(mountPath, "shell"),
|
|
20559
20572
|
kind: "device",
|
|
@@ -20562,6 +20575,7 @@ function nodesForHello(mountPath, deviceId, expose) {
|
|
|
20562
20575
|
});
|
|
20563
20576
|
}
|
|
20564
20577
|
if (expose.fs !== void 0) {
|
|
20578
|
+
rawTargets.push(joinTreePath(mountPath, "fs"));
|
|
20565
20579
|
nodes.push({
|
|
20566
20580
|
path: joinTreePath(mountPath, "fs"),
|
|
20567
20581
|
kind: "context",
|
|
@@ -20575,11 +20589,13 @@ function nodesForHello(mountPath, deviceId, expose) {
|
|
|
20575
20589
|
});
|
|
20576
20590
|
}
|
|
20577
20591
|
for (const raw of expose.nodes ?? []) {
|
|
20592
|
+
rawTargets.push(joinTreePath(mountPath, raw.path));
|
|
20578
20593
|
nodes.push(customNodeInput(mountPath, deviceId, raw));
|
|
20579
20594
|
}
|
|
20595
|
+
assertNoCollision(rawTargets, `\u8BBE\u5907 '${deviceId}' \u7684 expose \u8DEF\u5F84`);
|
|
20580
20596
|
return nodes;
|
|
20581
20597
|
}
|
|
20582
|
-
async function assertRegisterPath(registry, ctx, targetPath) {
|
|
20598
|
+
async function assertRegisterPath(registry, ctx, targetPath, reservedRoots) {
|
|
20583
20599
|
if (!check(ctx, targetPath, "register").allow) {
|
|
20584
20600
|
throw new TBError("permission_denied", `no scope grants 'register' on '${targetPath}'`);
|
|
20585
20601
|
}
|
|
@@ -20597,7 +20613,8 @@ async function assertRegisterPath(registry, ctx, targetPath) {
|
|
|
20597
20613
|
},
|
|
20598
20614
|
targetPath,
|
|
20599
20615
|
action: "write",
|
|
20600
|
-
existing
|
|
20616
|
+
existing,
|
|
20617
|
+
...reservedRoots !== void 0 ? { reservedRoots } : {}
|
|
20601
20618
|
});
|
|
20602
20619
|
if (!res.allow) throw res.error;
|
|
20603
20620
|
}
|
|
@@ -20618,7 +20635,7 @@ async function processDeviceHello(opts) {
|
|
|
20618
20635
|
const inputs = nodesForHello(mountPath, hello.deviceId, hello.expose);
|
|
20619
20636
|
const registry = new NodeRegistryStore(store);
|
|
20620
20637
|
for (const input of inputs) {
|
|
20621
|
-
await assertRegisterPath(registry, authCtx, input.path);
|
|
20638
|
+
await assertRegisterPath(registry, authCtx, input.path, opts.reservedRoots);
|
|
20622
20639
|
assertSecretRefUse(authCtx.scopes, input.config);
|
|
20623
20640
|
}
|
|
20624
20641
|
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -20633,7 +20650,7 @@ async function processDeviceHello(opts) {
|
|
|
20633
20650
|
input,
|
|
20634
20651
|
authCtx.keyId,
|
|
20635
20652
|
now,
|
|
20636
|
-
input.path === mountPath ? { online: true, lastSeenAt: now } : {}
|
|
20653
|
+
input.path === mountPath ? { deviceId: hello.deviceId, online: true, lastSeenAt: now } : {}
|
|
20637
20654
|
);
|
|
20638
20655
|
} catch (error) {
|
|
20639
20656
|
await searchSync?.abort(marker);
|
|
@@ -20647,6 +20664,80 @@ async function processDeviceHello(opts) {
|
|
|
20647
20664
|
return { mountPath, keyId: authCtx.keyId, searchIndexed };
|
|
20648
20665
|
}
|
|
20649
20666
|
|
|
20667
|
+
// ../app/src/deviceLifecycle.ts
|
|
20668
|
+
async function reverifyDeviceAuthority(opts) {
|
|
20669
|
+
const authCtx = await identify(opts.store, opts.authorization, (/* @__PURE__ */ new Date()).toISOString());
|
|
20670
|
+
if (authCtx === null || authCtx.keyId !== opts.keyId) return false;
|
|
20671
|
+
return checkRegisterPath({
|
|
20672
|
+
sk: {
|
|
20673
|
+
scopes: authCtx.scopes,
|
|
20674
|
+
id: authCtx.keyId,
|
|
20675
|
+
...authCtx.registerPaths !== void 0 ? { registerPaths: authCtx.registerPaths } : {}
|
|
20676
|
+
},
|
|
20677
|
+
targetPath: opts.mountPath,
|
|
20678
|
+
action: "write",
|
|
20679
|
+
existing: null,
|
|
20680
|
+
...opts.reservedRoots !== void 0 ? { reservedRoots: opts.reservedRoots } : {}
|
|
20681
|
+
}).allow;
|
|
20682
|
+
}
|
|
20683
|
+
async function markDeviceDisconnected(opts) {
|
|
20684
|
+
try {
|
|
20685
|
+
await opts.registry.setOnline(opts.mountPath, false, opts.now);
|
|
20686
|
+
} catch {
|
|
20687
|
+
}
|
|
20688
|
+
}
|
|
20689
|
+
async function reclaimDeviceSubtree(opts) {
|
|
20690
|
+
const searchSync = opts.search === void 0 ? void 0 : new SearchSynchronizer(opts.state, opts.search);
|
|
20691
|
+
const marker = await searchSync?.markSubtree(opts.mountPath);
|
|
20692
|
+
try {
|
|
20693
|
+
await opts.registry.deleteSubtree(opts.mountPath);
|
|
20694
|
+
} catch {
|
|
20695
|
+
}
|
|
20696
|
+
await searchSync?.removeSubtreeQuietly(opts.mountPath, marker);
|
|
20697
|
+
}
|
|
20698
|
+
function deviceSearchCapacityWarning(deviceId, mountPath) {
|
|
20699
|
+
return `[tool-bridge] device '${deviceId}' mounted at ${mountPath} but its tools are NOT in the search index: registry exceeds the tool-search capacity (TOOL_SEARCH_AUDIT_NODE_LIMIT). The device is callable but won't appear in tool search until capacity frees up.`;
|
|
20700
|
+
}
|
|
20701
|
+
|
|
20702
|
+
// ../app/src/deviceMailbox.ts
|
|
20703
|
+
var KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS = "sys:device-mailbox-cleanup-progress:v1";
|
|
20704
|
+
function parseProgress(value) {
|
|
20705
|
+
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");
|
|
20706
|
+
return value;
|
|
20707
|
+
}
|
|
20708
|
+
function createDeviceMailboxService(deps) {
|
|
20709
|
+
return new DeviceMailboxService(deps.state, deps.encryptionKey);
|
|
20710
|
+
}
|
|
20711
|
+
async function cleanupDeviceMailbox(deps, opts = {}) {
|
|
20712
|
+
await deps.ensureReady?.();
|
|
20713
|
+
if (deps.state.compareAndSwap === void 0) {
|
|
20714
|
+
throw new TBError("unavailable", "device mailbox cleanup requires StateStore.compareAndSwap");
|
|
20715
|
+
}
|
|
20716
|
+
const mailbox = createDeviceMailboxService(deps);
|
|
20717
|
+
const maxPages = Math.min(positiveInt(opts.maxPages, 8, "maxPages"), 64);
|
|
20718
|
+
const limit = Math.min(positiveInt(opts.limit, 200, "limit"), 200);
|
|
20719
|
+
const aggregate = { deleted: 0, expired: 0, scanned: 0 };
|
|
20720
|
+
const cursor = await runCursorCleanup({
|
|
20721
|
+
// {cursor,revision} 即持久形状,原样落库。
|
|
20722
|
+
encodeProgress: (progress) => progress,
|
|
20723
|
+
maxPages,
|
|
20724
|
+
parseProgress,
|
|
20725
|
+
progressKey: KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
|
|
20726
|
+
runPage: async (pageCursor) => {
|
|
20727
|
+
const page = await mailbox.cleanup({
|
|
20728
|
+
limit,
|
|
20729
|
+
...pageCursor === void 0 ? {} : { cursor: pageCursor }
|
|
20730
|
+
});
|
|
20731
|
+
aggregate.deleted += page.deleted;
|
|
20732
|
+
aggregate.expired += page.expired;
|
|
20733
|
+
aggregate.scanned += page.scanned;
|
|
20734
|
+
return page.cursor;
|
|
20735
|
+
},
|
|
20736
|
+
state: deps.state
|
|
20737
|
+
});
|
|
20738
|
+
return { ...aggregate, ...cursor === void 0 ? {} : { cursor } };
|
|
20739
|
+
}
|
|
20740
|
+
|
|
20650
20741
|
// ../../node_modules/.pnpm/@modelcontextprotocol+server@2.0.0/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
20651
20742
|
var __create2 = Object.create;
|
|
20652
20743
|
var __defProp2 = Object.defineProperty;
|
|
@@ -36292,17 +36383,34 @@ function assertSchemaShape(value, path, depth = 0) {
|
|
|
36292
36383
|
list.forEach((sub, i) => assertSchemaShape(sub, `${path}/${key}/${i}`, depth + 1));
|
|
36293
36384
|
}
|
|
36294
36385
|
}
|
|
36295
|
-
function inputSchemaOf(raw) {
|
|
36296
|
-
if (raw === void 0) return { type: "object", properties: {} };
|
|
36297
|
-
|
|
36386
|
+
function inputSchemaOf(raw, delivery) {
|
|
36387
|
+
if (raw === void 0 && delivery === void 0) return { type: "object", properties: {} };
|
|
36388
|
+
const source = raw ?? { type: "object", properties: {} };
|
|
36389
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
36298
36390
|
throw new Error("MCP tool inputSchema must be a JSON Schema object");
|
|
36299
36391
|
}
|
|
36300
|
-
const schema =
|
|
36392
|
+
const schema = source;
|
|
36301
36393
|
if (schema.type !== void 0 && schema.type !== "object") {
|
|
36302
36394
|
throw new Error("MCP tool inputSchema root type must be object");
|
|
36303
36395
|
}
|
|
36304
36396
|
assertSchemaShape(schema, "inputSchema");
|
|
36305
|
-
|
|
36397
|
+
if (delivery !== "mailbox" && delivery !== "both") {
|
|
36398
|
+
return { ...schema, type: "object" };
|
|
36399
|
+
}
|
|
36400
|
+
const properties = schema.properties;
|
|
36401
|
+
if (properties !== void 0 && (properties === null || typeof properties !== "object" || Array.isArray(properties))) throw new Error("MCP tool inputSchema properties must be an object");
|
|
36402
|
+
return {
|
|
36403
|
+
...schema,
|
|
36404
|
+
type: "object",
|
|
36405
|
+
properties: {
|
|
36406
|
+
...properties ?? {},
|
|
36407
|
+
"~delivery": {
|
|
36408
|
+
type: "string",
|
|
36409
|
+
enum: delivery === "both" ? ["realtime", "mailbox", "fallback"] : ["mailbox", "fallback"],
|
|
36410
|
+
description: "Tool Bridge delivery policy; fallback prefers realtime and queues only when dispatch definitely did not occur."
|
|
36411
|
+
}
|
|
36412
|
+
}
|
|
36413
|
+
};
|
|
36306
36414
|
}
|
|
36307
36415
|
function annotationsOf(tool) {
|
|
36308
36416
|
const annotations = {};
|
|
@@ -36328,7 +36436,7 @@ async function projectTools(source, validator) {
|
|
|
36328
36436
|
HTBP ${bridge.invokePath}`;
|
|
36329
36437
|
let inputSchema;
|
|
36330
36438
|
try {
|
|
36331
|
-
inputSchema = inputSchemaOf(bridge.inputSchema);
|
|
36439
|
+
inputSchema = inputSchemaOf(bridge.inputSchema, bridge.delivery);
|
|
36332
36440
|
} catch {
|
|
36333
36441
|
throw new ProtocolError3(
|
|
36334
36442
|
ProtocolErrorCode2.InternalError,
|
|
@@ -36706,6 +36814,16 @@ import { Hono } from "hono";
|
|
|
36706
36814
|
// ../core/src/protocol/openapi.ts
|
|
36707
36815
|
var schemaRegistry = external_exports.registry();
|
|
36708
36816
|
var schemas = {
|
|
36817
|
+
DeviceOperationClaimRequest: deviceOperationClaimRequestSchema,
|
|
36818
|
+
DeviceOperationClaimResponse: deviceOperationClaimResponseSchema,
|
|
36819
|
+
DeviceOperationCompleteRequest: deviceOperationCompleteRequestSchema,
|
|
36820
|
+
DeviceOperationDetail: deviceOperationDetailSchema,
|
|
36821
|
+
DeviceOperationIdentityRequest: deviceOperationIdentityRequestSchema,
|
|
36822
|
+
DeviceOperationListRequest: deviceOperationListRequestSchema,
|
|
36823
|
+
DeviceOperationListResponse: deviceOperationListResponseSchema,
|
|
36824
|
+
DeviceOperationRenewRequest: deviceOperationRenewRequestSchema,
|
|
36825
|
+
DeviceOperationRenewResponse: deviceOperationRenewResponseSchema,
|
|
36826
|
+
DeviceOperationSummary: deviceOperationSummarySchema,
|
|
36709
36827
|
Error: tbErrorBodySchema,
|
|
36710
36828
|
FeedbackDetail: feedbackDetailSchema,
|
|
36711
36829
|
FeedbackList: feedbackListSchema,
|
|
@@ -36941,20 +37059,82 @@ var fixedControlPlaneOpenApi = {
|
|
|
36941
37059
|
"x-tool-bridge-greedy-path": true
|
|
36942
37060
|
}
|
|
36943
37061
|
},
|
|
37062
|
+
"/~device/operations/get": {
|
|
37063
|
+
post: {
|
|
37064
|
+
operationId: "getDeviceOperation",
|
|
37065
|
+
security: bearerSecurity,
|
|
37066
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationIdentityRequest") },
|
|
37067
|
+
responses: { 200: jsonResponse("Device operation detail", "DeviceOperationDetail"), ...errorResponses }
|
|
37068
|
+
}
|
|
37069
|
+
},
|
|
37070
|
+
"/~device/operations/list": {
|
|
37071
|
+
post: {
|
|
37072
|
+
operationId: "listDeviceOperations",
|
|
37073
|
+
security: bearerSecurity,
|
|
37074
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationListRequest") },
|
|
37075
|
+
responses: { 200: jsonResponse("Visible device operation page", "DeviceOperationListResponse"), ...errorResponses }
|
|
37076
|
+
}
|
|
37077
|
+
},
|
|
37078
|
+
"/~device/operations/cancel": {
|
|
37079
|
+
post: {
|
|
37080
|
+
operationId: "cancelDeviceOperation",
|
|
37081
|
+
security: bearerSecurity,
|
|
37082
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationIdentityRequest") },
|
|
37083
|
+
responses: { 200: jsonResponse("Cancelled or cancellation-requested operation", "DeviceOperationDetail"), ...errorResponses }
|
|
37084
|
+
}
|
|
37085
|
+
},
|
|
37086
|
+
"/~device/mailbox/claim": {
|
|
37087
|
+
post: {
|
|
37088
|
+
operationId: "claimDeviceOperation",
|
|
37089
|
+
security: bearerSecurity,
|
|
37090
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationClaimRequest") },
|
|
37091
|
+
responses: { 200: jsonResponse("Claimed operation or next scan cursor", "DeviceOperationClaimResponse"), ...errorResponses }
|
|
37092
|
+
}
|
|
37093
|
+
},
|
|
37094
|
+
"/~device/mailbox/renew": {
|
|
37095
|
+
post: {
|
|
37096
|
+
operationId: "renewDeviceOperation",
|
|
37097
|
+
security: bearerSecurity,
|
|
37098
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationRenewRequest") },
|
|
37099
|
+
responses: { 200: jsonResponse("Renewed operation lease", "DeviceOperationRenewResponse"), ...errorResponses }
|
|
37100
|
+
}
|
|
37101
|
+
},
|
|
37102
|
+
"/~device/mailbox/complete": {
|
|
37103
|
+
post: {
|
|
37104
|
+
operationId: "completeDeviceOperation",
|
|
37105
|
+
security: bearerSecurity,
|
|
37106
|
+
requestBody: { required: true, content: jsonContent("DeviceOperationCompleteRequest") },
|
|
37107
|
+
responses: { 200: jsonResponse("Terminal device operation", "DeviceOperationDetail"), ...errorResponses }
|
|
37108
|
+
}
|
|
37109
|
+
},
|
|
36944
37110
|
"/{commandPath}": {
|
|
36945
37111
|
post: {
|
|
36946
37112
|
"operationId": "invoke",
|
|
36947
37113
|
"security": bearerSecurity,
|
|
36948
|
-
"parameters": [
|
|
36949
|
-
|
|
36950
|
-
|
|
36951
|
-
|
|
36952
|
-
|
|
36953
|
-
|
|
36954
|
-
|
|
37114
|
+
"parameters": [
|
|
37115
|
+
{
|
|
37116
|
+
in: "path",
|
|
37117
|
+
name: "commandPath",
|
|
37118
|
+
required: true,
|
|
37119
|
+
schema: { type: "string" },
|
|
37120
|
+
description: "Complete HTBP command path, including the command/tool leaf."
|
|
37121
|
+
},
|
|
37122
|
+
{
|
|
37123
|
+
in: "query",
|
|
37124
|
+
name: "ttlSeconds",
|
|
37125
|
+
schema: { type: "integer", minimum: 1 },
|
|
37126
|
+
description: "Mailbox operation TTL; used only when ~delivery can enqueue."
|
|
37127
|
+
},
|
|
37128
|
+
{
|
|
37129
|
+
in: "header",
|
|
37130
|
+
name: "x-tb-idempotency-key",
|
|
37131
|
+
schema: { type: "string", minLength: 1, maxLength: 255 },
|
|
37132
|
+
description: "Mailbox retry key; used only when ~delivery can enqueue."
|
|
37133
|
+
}
|
|
37134
|
+
],
|
|
36955
37135
|
"requestBody": {
|
|
36956
37136
|
required: true,
|
|
36957
|
-
description: "Raw command arguments
|
|
37137
|
+
description: "Raw command arguments plus the reserved optional ~delivery control.",
|
|
36958
37138
|
content: { "application/json": { schema: {} } }
|
|
36959
37139
|
},
|
|
36960
37140
|
"responses": {
|
|
@@ -36965,6 +37145,7 @@ var fixedControlPlaneOpenApi = {
|
|
|
36965
37145
|
"text/markdown": { schema: { type: "string" } }
|
|
36966
37146
|
}
|
|
36967
37147
|
},
|
|
37148
|
+
202: jsonResponse("Durably accepted device operation", "DeviceOperationDetail"),
|
|
36968
37149
|
...errorResponses
|
|
36969
37150
|
},
|
|
36970
37151
|
"x-tool-bridge-greedy-path": true
|
|
@@ -37250,6 +37431,239 @@ function renderTreeDsl(tree) {
|
|
|
37250
37431
|
`;
|
|
37251
37432
|
}
|
|
37252
37433
|
|
|
37434
|
+
// ../app/src/routes/deviceMailbox.ts
|
|
37435
|
+
var DEVICE_MAILBOX_IDEMPOTENCY_HEADER = "x-tb-idempotency-key";
|
|
37436
|
+
var DEVICE_MAILBOX_HTTP_BODY_BYTES = 272 * 1024;
|
|
37437
|
+
var DEVICE_MAILBOX_LIST_LIMIT_DEFAULT = 50;
|
|
37438
|
+
var DEVICE_MAILBOX_LIST_LIMIT_MAX = 200;
|
|
37439
|
+
function invalidRequest(message) {
|
|
37440
|
+
return new TBError("invalid_argument", message);
|
|
37441
|
+
}
|
|
37442
|
+
async function mailboxJsonObject(c, opts = {}) {
|
|
37443
|
+
const declaredLength = Number(c.req.header("content-length"));
|
|
37444
|
+
if (Number.isFinite(declaredLength) && declaredLength > DEVICE_MAILBOX_HTTP_BODY_BYTES) {
|
|
37445
|
+
throw new TBError("rate_limited", "device mailbox request body is too large", {
|
|
37446
|
+
retryable: false
|
|
37447
|
+
});
|
|
37448
|
+
}
|
|
37449
|
+
const reader = c.req.raw.body?.getReader();
|
|
37450
|
+
let raw = "";
|
|
37451
|
+
if (reader !== void 0) {
|
|
37452
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
37453
|
+
let received = 0;
|
|
37454
|
+
try {
|
|
37455
|
+
while (true) {
|
|
37456
|
+
const chunk = await reader.read();
|
|
37457
|
+
if (chunk.done) break;
|
|
37458
|
+
received += chunk.value.byteLength;
|
|
37459
|
+
if (received > DEVICE_MAILBOX_HTTP_BODY_BYTES) {
|
|
37460
|
+
await reader.cancel().catch(() => {
|
|
37461
|
+
});
|
|
37462
|
+
throw new TBError("rate_limited", "device mailbox request body is too large", {
|
|
37463
|
+
retryable: false
|
|
37464
|
+
});
|
|
37465
|
+
}
|
|
37466
|
+
raw += decoder.decode(chunk.value, { stream: true });
|
|
37467
|
+
}
|
|
37468
|
+
raw += decoder.decode();
|
|
37469
|
+
} catch (cause) {
|
|
37470
|
+
if (cause instanceof TBError) throw cause;
|
|
37471
|
+
throw invalidRequest("body must be valid UTF-8 JSON");
|
|
37472
|
+
}
|
|
37473
|
+
}
|
|
37474
|
+
let parsed = opts.allowEmpty === true && raw === "" ? {} : null;
|
|
37475
|
+
if (raw !== "") {
|
|
37476
|
+
try {
|
|
37477
|
+
parsed = JSON.parse(raw);
|
|
37478
|
+
} catch {
|
|
37479
|
+
parsed = null;
|
|
37480
|
+
}
|
|
37481
|
+
}
|
|
37482
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
37483
|
+
throw invalidRequest("body must be a JSON object");
|
|
37484
|
+
}
|
|
37485
|
+
return parsed;
|
|
37486
|
+
}
|
|
37487
|
+
function parseBody(schema, body) {
|
|
37488
|
+
const parsed = schema.safeParse(body);
|
|
37489
|
+
if (!parsed.success) throw invalidRequest(parsed.error.issues[0]?.message ?? "invalid request");
|
|
37490
|
+
return parsed.data;
|
|
37491
|
+
}
|
|
37492
|
+
function deviceOperationTtlSeconds(url2) {
|
|
37493
|
+
for (const key of url2.searchParams.keys()) {
|
|
37494
|
+
if (key !== "ttlSeconds") throw invalidRequest(`unknown enqueue query parameter '${key}'`);
|
|
37495
|
+
}
|
|
37496
|
+
const value = url2.searchParams.get("ttlSeconds");
|
|
37497
|
+
if (value === null) return void 0;
|
|
37498
|
+
if (!/^[1-9]\d*$/.test(value)) throw invalidRequest("ttlSeconds must be a positive integer");
|
|
37499
|
+
const parsed = Number(value);
|
|
37500
|
+
if (!Number.isSafeInteger(parsed)) throw invalidRequest("ttlSeconds must be a safe integer");
|
|
37501
|
+
return parsed;
|
|
37502
|
+
}
|
|
37503
|
+
function canManage(ctx, operation) {
|
|
37504
|
+
return ctx.owner === operation.caller.owner || check(ctx, operation.targetPath, "admin").allow;
|
|
37505
|
+
}
|
|
37506
|
+
function assertCanManage(ctx, operation) {
|
|
37507
|
+
if (!canManage(ctx, operation)) throw TBError.notFound("device operation not found");
|
|
37508
|
+
}
|
|
37509
|
+
async function assertCurrentDeviceCredential(c, target, reservedRoots) {
|
|
37510
|
+
const ctx = c.get("ctx");
|
|
37511
|
+
if (ctx.keyId !== target.deviceKeyId) {
|
|
37512
|
+
throw new TBError("permission_denied", "device credential does not own this operation");
|
|
37513
|
+
}
|
|
37514
|
+
let existing = null;
|
|
37515
|
+
try {
|
|
37516
|
+
existing = await new NodeRegistryStore(c.get("store")).get(target.mountPath);
|
|
37517
|
+
} catch {
|
|
37518
|
+
existing = null;
|
|
37519
|
+
}
|
|
37520
|
+
const allowed = checkRegisterPath({
|
|
37521
|
+
action: "write",
|
|
37522
|
+
existing,
|
|
37523
|
+
sk: {
|
|
37524
|
+
id: ctx.keyId,
|
|
37525
|
+
scopes: ctx.scopes,
|
|
37526
|
+
...ctx.registerPaths === void 0 ? {} : { registerPaths: ctx.registerPaths }
|
|
37527
|
+
},
|
|
37528
|
+
targetPath: target.mountPath,
|
|
37529
|
+
...reservedRoots === void 0 ? {} : { reservedRoots }
|
|
37530
|
+
});
|
|
37531
|
+
if (!allowed.allow) throw allowed.error;
|
|
37532
|
+
}
|
|
37533
|
+
async function enqueueDeviceCommand(c, env, input) {
|
|
37534
|
+
const { arguments: args, command, marker, node } = input;
|
|
37535
|
+
const ctx = c.get("ctx");
|
|
37536
|
+
const result = await env.mailbox().enqueue({
|
|
37537
|
+
arguments: args,
|
|
37538
|
+
caller: { keyId: ctx.keyId, owner: ctx.owner },
|
|
37539
|
+
deviceId: marker.deviceId,
|
|
37540
|
+
deviceKeyId: node.registeredBy,
|
|
37541
|
+
mountPath: marker.mountPath,
|
|
37542
|
+
// 管理面与审计链必须能区分同一 tool 节点下的不同命令;权限仍按前缀 scope 判定。
|
|
37543
|
+
targetPath: `${node.path}/${command}`,
|
|
37544
|
+
path: `${relativeDevicePath(node.path, marker.mountPath)}/${command}`,
|
|
37545
|
+
traceId: ctx.traceId,
|
|
37546
|
+
...c.req.header(DEVICE_MAILBOX_IDEMPOTENCY_HEADER) === void 0 ? {} : { idempotencyKey: c.req.header(DEVICE_MAILBOX_IDEMPOTENCY_HEADER) },
|
|
37547
|
+
...input.ttlSeconds === void 0 ? {} : { ttlSeconds: input.ttlSeconds }
|
|
37548
|
+
});
|
|
37549
|
+
const response = c.json(deviceOperationDetailSchema.parse(result), 202);
|
|
37550
|
+
response.headers.set("x-tb-delivery", "mailbox");
|
|
37551
|
+
return response;
|
|
37552
|
+
}
|
|
37553
|
+
async function getOperation(c, env) {
|
|
37554
|
+
const input = parseBody(deviceOperationIdentityRequestSchema, await mailboxJsonObject(c));
|
|
37555
|
+
const result = await env.mailbox().get(input.deviceId, input.operationId);
|
|
37556
|
+
assertCanManage(c.get("ctx"), result);
|
|
37557
|
+
return c.json(deviceOperationDetailSchema.parse(result));
|
|
37558
|
+
}
|
|
37559
|
+
async function listOperations(c, env) {
|
|
37560
|
+
const input = parseBody(deviceOperationListRequestSchema, await mailboxJsonObject(c));
|
|
37561
|
+
const mailbox = env.mailbox();
|
|
37562
|
+
const ctx = c.get("ctx");
|
|
37563
|
+
const limit = Math.min(
|
|
37564
|
+
input.opts?.limit ?? DEVICE_MAILBOX_LIST_LIMIT_DEFAULT,
|
|
37565
|
+
DEVICE_MAILBOX_LIST_LIMIT_MAX
|
|
37566
|
+
);
|
|
37567
|
+
let cursor = input.opts?.cursor;
|
|
37568
|
+
const items = [];
|
|
37569
|
+
while (items.length < limit) {
|
|
37570
|
+
const previous = cursor;
|
|
37571
|
+
const page = await mailbox.list({
|
|
37572
|
+
deviceId: input.deviceId,
|
|
37573
|
+
limit: limit - items.length,
|
|
37574
|
+
...cursor === void 0 ? {} : { cursor },
|
|
37575
|
+
...input.opts?.states === void 0 ? {} : { states: input.opts.states }
|
|
37576
|
+
});
|
|
37577
|
+
items.push(...page.items.filter((item) => canManage(ctx, item)));
|
|
37578
|
+
cursor = page.cursor;
|
|
37579
|
+
if (cursor === void 0) {
|
|
37580
|
+
return c.json(deviceOperationListResponseSchema.parse({ items }));
|
|
37581
|
+
}
|
|
37582
|
+
if (cursor === previous) {
|
|
37583
|
+
throw new TBError("internal", "device mailbox list cursor did not advance");
|
|
37584
|
+
}
|
|
37585
|
+
}
|
|
37586
|
+
const resumeCursor = cursor;
|
|
37587
|
+
if (resumeCursor === void 0) {
|
|
37588
|
+
return c.json(deviceOperationListResponseSchema.parse({ items }));
|
|
37589
|
+
}
|
|
37590
|
+
let probeCursor = resumeCursor;
|
|
37591
|
+
while (probeCursor !== void 0) {
|
|
37592
|
+
const previous = probeCursor;
|
|
37593
|
+
const page = await mailbox.list({
|
|
37594
|
+
deviceId: input.deviceId,
|
|
37595
|
+
cursor: probeCursor,
|
|
37596
|
+
limit: DEVICE_MAILBOX_LIST_LIMIT_MAX,
|
|
37597
|
+
...input.opts?.states === void 0 ? {} : { states: input.opts.states }
|
|
37598
|
+
});
|
|
37599
|
+
if (page.items.some((item) => canManage(ctx, item))) {
|
|
37600
|
+
return c.json(deviceOperationListResponseSchema.parse({ items, cursor: resumeCursor }));
|
|
37601
|
+
}
|
|
37602
|
+
probeCursor = page.cursor;
|
|
37603
|
+
if (probeCursor === previous) {
|
|
37604
|
+
throw new TBError("internal", "device mailbox list cursor did not advance");
|
|
37605
|
+
}
|
|
37606
|
+
}
|
|
37607
|
+
return c.json(deviceOperationListResponseSchema.parse({ items }));
|
|
37608
|
+
}
|
|
37609
|
+
async function cancelOperation(c, env) {
|
|
37610
|
+
const input = parseBody(deviceOperationIdentityRequestSchema, await mailboxJsonObject(c));
|
|
37611
|
+
const mailbox = env.mailbox();
|
|
37612
|
+
const current = await mailbox.get(input.deviceId, input.operationId);
|
|
37613
|
+
assertCanManage(c.get("ctx"), current);
|
|
37614
|
+
return c.json(deviceOperationDetailSchema.parse(
|
|
37615
|
+
await mailbox.cancel(input.deviceId, input.operationId)
|
|
37616
|
+
));
|
|
37617
|
+
}
|
|
37618
|
+
function deviceAuthorize(c, env) {
|
|
37619
|
+
return async (target) => await assertCurrentDeviceCredential(c, target, env.deps.reservedRoots);
|
|
37620
|
+
}
|
|
37621
|
+
async function claimOperations(c, env) {
|
|
37622
|
+
const input = parseBody(deviceOperationClaimRequestSchema, await mailboxJsonObject(c));
|
|
37623
|
+
return c.json(deviceOperationClaimResponseSchema.parse(await env.mailbox().claim({
|
|
37624
|
+
...input,
|
|
37625
|
+
deviceKeyId: c.get("ctx").keyId,
|
|
37626
|
+
authorize: deviceAuthorize(c, env)
|
|
37627
|
+
})));
|
|
37628
|
+
}
|
|
37629
|
+
async function renewLeases(c, env) {
|
|
37630
|
+
const input = parseBody(deviceOperationRenewRequestSchema, await mailboxJsonObject(c));
|
|
37631
|
+
return c.json(deviceOperationRenewResponseSchema.parse(await env.mailbox().renew({
|
|
37632
|
+
...input,
|
|
37633
|
+
deviceKeyId: c.get("ctx").keyId,
|
|
37634
|
+
authorize: deviceAuthorize(c, env)
|
|
37635
|
+
})));
|
|
37636
|
+
}
|
|
37637
|
+
async function completeOperation(c, env) {
|
|
37638
|
+
const input = parseBody(deviceOperationCompleteRequestSchema, await mailboxJsonObject(c));
|
|
37639
|
+
let completion;
|
|
37640
|
+
if (input.outcome === "succeeded") {
|
|
37641
|
+
completion = { outcome: "succeeded", result: input.result };
|
|
37642
|
+
} else if (input.outcome === "result_unknown") {
|
|
37643
|
+
completion = {
|
|
37644
|
+
outcome: "result_unknown",
|
|
37645
|
+
...input.error === void 0 ? {} : { error: input.error }
|
|
37646
|
+
};
|
|
37647
|
+
} else {
|
|
37648
|
+
completion = { outcome: input.outcome, error: input.error };
|
|
37649
|
+
}
|
|
37650
|
+
return c.json(deviceOperationDetailSchema.parse(await env.mailbox().complete({
|
|
37651
|
+
deviceId: input.deviceId,
|
|
37652
|
+
operationId: input.operationId,
|
|
37653
|
+
leaseId: input.leaseId,
|
|
37654
|
+
deviceKeyId: c.get("ctx").keyId,
|
|
37655
|
+
authorize: deviceAuthorize(c, env)
|
|
37656
|
+
}, completion)));
|
|
37657
|
+
}
|
|
37658
|
+
function registerDeviceMailboxRoutes(app, env) {
|
|
37659
|
+
app.post("/~device/operations/get", (c) => runHandler(async () => await getOperation(c, env)));
|
|
37660
|
+
app.post("/~device/operations/list", (c) => runHandler(async () => await listOperations(c, env)));
|
|
37661
|
+
app.post("/~device/operations/cancel", (c) => runHandler(async () => await cancelOperation(c, env)));
|
|
37662
|
+
app.post("/~device/mailbox/claim", (c) => runHandler(async () => await claimOperations(c, env)));
|
|
37663
|
+
app.post("/~device/mailbox/renew", (c) => runHandler(async () => await renewLeases(c, env)));
|
|
37664
|
+
app.post("/~device/mailbox/complete", (c) => runHandler(async () => await completeOperation(c, env)));
|
|
37665
|
+
}
|
|
37666
|
+
|
|
37253
37667
|
// ../app/src/federation.ts
|
|
37254
37668
|
var SEARCH_REMAINING_MS_HEADER = "x-tb-search-remaining-ms";
|
|
37255
37669
|
function remoteProtocolError(message) {
|
|
@@ -37471,6 +37885,24 @@ function assertRemoteConfigAllowed(config, settings) {
|
|
|
37471
37885
|
assertRemoteAllowed(baseUrl, settings);
|
|
37472
37886
|
}
|
|
37473
37887
|
|
|
37888
|
+
// ../app/src/registryMutation.ts
|
|
37889
|
+
async function assertNodeConfigMutation(opts) {
|
|
37890
|
+
const { action, config, ctx, deps, registry, targetPath } = opts;
|
|
37891
|
+
assertRemoteConfigAllowed(config, await resolveRemoteSettings(deps.state, deps.remote));
|
|
37892
|
+
await assertRegisterPath2(registry, ctx, targetPath, action, deps);
|
|
37893
|
+
assertSecretRefUse(ctx.scopes, config);
|
|
37894
|
+
await assertMcpOAuthConfig(config, deps.secrets);
|
|
37895
|
+
await assertContextConfig(config, deps);
|
|
37896
|
+
await assertSkillhubConfig(config, deps);
|
|
37897
|
+
await assertToolConfig(config, deps, ctx, targetPath);
|
|
37898
|
+
}
|
|
37899
|
+
async function invalidateNodeDerivedState(store, path) {
|
|
37900
|
+
await invalidateToolCache(store, path);
|
|
37901
|
+
await invalidateMcpEra(store, path);
|
|
37902
|
+
await invalidateMcpOAuth(store, path);
|
|
37903
|
+
await invalidateProviderOAuth(store, path);
|
|
37904
|
+
}
|
|
37905
|
+
|
|
37474
37906
|
// ../app/src/routes/register.ts
|
|
37475
37907
|
async function authorizeToolNode(c, env, node, encryptionKey) {
|
|
37476
37908
|
const { deps } = env;
|
|
@@ -37527,16 +37959,17 @@ async function handleRegister(c, env) {
|
|
|
37527
37959
|
);
|
|
37528
37960
|
}
|
|
37529
37961
|
const body = parseNodeInput(raw);
|
|
37530
|
-
assertRemoteConfigAllowed(body.config, await resolveRemoteSettings(store, deps.remote));
|
|
37531
37962
|
if (!check(ctx, path, "register").allow) {
|
|
37532
37963
|
throw new TBError("permission_denied", `no scope grants 'register' on '${path}'`);
|
|
37533
37964
|
}
|
|
37534
|
-
await
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
|
|
37539
|
-
|
|
37965
|
+
await assertNodeConfigMutation({
|
|
37966
|
+
action: "write",
|
|
37967
|
+
config: body.config,
|
|
37968
|
+
ctx,
|
|
37969
|
+
deps,
|
|
37970
|
+
registry,
|
|
37971
|
+
targetPath: body.path
|
|
37972
|
+
});
|
|
37540
37973
|
await searchSync?.ensureSeeded();
|
|
37541
37974
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
37542
37975
|
const marker = await searchSync?.markNode(body.path);
|
|
@@ -37547,12 +37980,9 @@ async function handleRegister(c, env) {
|
|
|
37547
37980
|
await searchSync?.abort(marker);
|
|
37548
37981
|
throw error;
|
|
37549
37982
|
}
|
|
37550
|
-
await
|
|
37551
|
-
await invalidateMcpEra(store, body.path);
|
|
37552
|
-
await invalidateMcpOAuth(store, body.path);
|
|
37553
|
-
await invalidateProviderOAuth(store, body.path);
|
|
37983
|
+
await invalidateNodeDerivedState(store, body.path);
|
|
37554
37984
|
await searchSync?.reconcileNodeQuietly(body.path, { marker });
|
|
37555
|
-
if (await refreshDynamicSearchNode(node, ctx, deps)) await searchSync?.abort(marker);
|
|
37985
|
+
if (await refreshDynamicSearchNode(node, ctx, deps, searchSync)) await searchSync?.abort(marker);
|
|
37556
37986
|
return new Response(JSON.stringify(registryNodeSchema.parse(node)), {
|
|
37557
37987
|
headers: { "content-type": contentTypeFor("json") }
|
|
37558
37988
|
});
|
|
@@ -37649,7 +38079,7 @@ async function handleDescribe(c, env) {
|
|
|
37649
38079
|
await assertContextAlive(node, node.config, registry);
|
|
37650
38080
|
const cfg = node.config;
|
|
37651
38081
|
const local = cfg.provider !== "r2" && cfg.provider !== "s3" ? localContext(deps, node) : null;
|
|
37652
|
-
let capabilities = cfg.provider === "r2" || cfg.provider === "s3" || cfg.provider === "device-fs" || deviceMarkerOf(cfg.providerConfig) !== null ? CONTEXT_CAPABILITIES : local !== null ?
|
|
38082
|
+
let capabilities = cfg.provider === "r2" || cfg.provider === "s3" || cfg.provider === "device-fs" || deviceMarkerOf(cfg.providerConfig) !== null ? CONTEXT_CAPABILITIES : local !== null ? contextCapabilitiesOf(local) : (await requirePluginExport(deps, cfg.provider, "context", "context", cfg.export)).export.capabilities ?? [];
|
|
37653
38083
|
if ((cfg.provider === "r2" || cfg.provider === "s3") && cfg.readOnly !== true && await contextDirectUploadAvailable(cfg, deps)) {
|
|
37654
38084
|
capabilities = [...capabilities, CONTEXT_DIRECT_UPLOAD_CAPABILITY];
|
|
37655
38085
|
}
|
|
@@ -38317,12 +38747,8 @@ var FederatedSearchSessionStore = class {
|
|
|
38317
38747
|
}
|
|
38318
38748
|
};
|
|
38319
38749
|
|
|
38320
|
-
// ../app/src/search/
|
|
38321
|
-
function
|
|
38322
|
-
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
38323
|
-
}
|
|
38324
|
-
function projectSearchTool(tool, detail) {
|
|
38325
|
-
if (detail === "full") return tool;
|
|
38750
|
+
// ../app/src/search/compactTool.ts
|
|
38751
|
+
function compactSearchTool(tool) {
|
|
38326
38752
|
const compact = { ...tool };
|
|
38327
38753
|
delete compact.inputSchema;
|
|
38328
38754
|
delete compact.outputSchema;
|
|
@@ -38342,6 +38768,14 @@ function projectSearchTool(tool, detail) {
|
|
|
38342
38768
|
}
|
|
38343
38769
|
return compact;
|
|
38344
38770
|
}
|
|
38771
|
+
|
|
38772
|
+
// ../app/src/search/localSearch.ts
|
|
38773
|
+
function normalizedEffect(effect) {
|
|
38774
|
+
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
38775
|
+
}
|
|
38776
|
+
function projectSearchTool(tool, detail) {
|
|
38777
|
+
return detail === "full" ? tool : compactSearchTool(tool);
|
|
38778
|
+
}
|
|
38345
38779
|
async function executeLocalSearch(deps, request) {
|
|
38346
38780
|
const { ctx, search, searchSync, state } = deps;
|
|
38347
38781
|
const query = request.query;
|
|
@@ -38497,24 +38931,7 @@ function continuationSourceError(error) {
|
|
|
38497
38931
|
return retryableContinuationError();
|
|
38498
38932
|
}
|
|
38499
38933
|
function compactItem(item) {
|
|
38500
|
-
|
|
38501
|
-
delete tool.inputSchema;
|
|
38502
|
-
delete tool.outputSchema;
|
|
38503
|
-
if (tool.description !== void 0) {
|
|
38504
|
-
const encoder = new TextEncoder();
|
|
38505
|
-
if (encoder.encode(tool.description).length > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) {
|
|
38506
|
-
let description = "";
|
|
38507
|
-
let bytes = 0;
|
|
38508
|
-
for (const char of tool.description) {
|
|
38509
|
-
const size = encoder.encode(char).length;
|
|
38510
|
-
if (bytes + size > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) break;
|
|
38511
|
-
description += char;
|
|
38512
|
-
bytes += size;
|
|
38513
|
-
}
|
|
38514
|
-
tool.description = description;
|
|
38515
|
-
}
|
|
38516
|
-
}
|
|
38517
|
-
return { ...item, tool };
|
|
38934
|
+
return { ...item, tool: compactSearchTool(item.tool) };
|
|
38518
38935
|
}
|
|
38519
38936
|
function normalizedEffect2(effect) {
|
|
38520
38937
|
return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
|
|
@@ -39214,9 +39631,6 @@ async function executeFederatedSearch(deps, input) {
|
|
|
39214
39631
|
"recursive search requires explicit TB_INSTANCE_ID and atomic StateStore CAS"
|
|
39215
39632
|
);
|
|
39216
39633
|
}
|
|
39217
|
-
if ((input.request.opts?.mode ?? "keyword") !== "keyword") {
|
|
39218
|
-
throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
|
|
39219
|
-
}
|
|
39220
39634
|
const wireOpts = input.request.opts ?? {};
|
|
39221
39635
|
const constraints = normalizeToolSearchOptions({
|
|
39222
39636
|
...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
|
|
@@ -39529,13 +39943,6 @@ function registerSearchRoute(app, env) {
|
|
|
39529
39943
|
);
|
|
39530
39944
|
}
|
|
39531
39945
|
const request = requestWithNormalizedLimit(parsed.data);
|
|
39532
|
-
const mode = request.opts?.mode ?? "keyword";
|
|
39533
|
-
if (mode === "semantic" && !capabilities.includes("search:semantic")) {
|
|
39534
|
-
throw new TBError(
|
|
39535
|
-
"invalid_argument",
|
|
39536
|
-
"search mode 'semantic' requires capability 'search:semantic'"
|
|
39537
|
-
);
|
|
39538
|
-
}
|
|
39539
39946
|
const explicitFederation = request.opts?.federation;
|
|
39540
39947
|
const cursor = request.opts?.cursor;
|
|
39541
39948
|
const federation = explicitFederation ?? (cursor !== void 0 && !isFederatedSearchSessionHandle(cursor) ? "local" : capabilities.includes("search:federated") ? "recursive" : "local");
|
|
@@ -39545,9 +39952,6 @@ function registerSearchRoute(app, env) {
|
|
|
39545
39952
|
"recursive search requires capability 'search:federated'"
|
|
39546
39953
|
);
|
|
39547
39954
|
}
|
|
39548
|
-
if (federation === "recursive" && mode === "semantic") {
|
|
39549
|
-
throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
|
|
39550
|
-
}
|
|
39551
39955
|
const normalizedRequest = {
|
|
39552
39956
|
...request,
|
|
39553
39957
|
opts: { ...request.opts ?? {}, federation }
|
|
@@ -39615,7 +40019,7 @@ async function helpModelFor(node, registry, ctx, builtins, deps, opts) {
|
|
|
39615
40019
|
}
|
|
39616
40020
|
if (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") {
|
|
39617
40021
|
const provider = await providerFor(node, ctx, deps);
|
|
39618
|
-
const raw = await upstreamTools(node, provider, deps, opts.refresh, opts.now);
|
|
40022
|
+
const raw = await upstreamTools(node, provider, deps, opts.refresh, opts.now, opts.searchSync);
|
|
39619
40023
|
const { exposed } = virtualizeTools(node.virtualize, raw);
|
|
39620
40024
|
return toolsToHelpModel(
|
|
39621
40025
|
node.path,
|
|
@@ -39685,7 +40089,8 @@ async function commandHelpModelFor(registry, ctx, builtins, deps, path, opts) {
|
|
|
39685
40089
|
const full = await helpModelFor(node, registry, ctx, builtins, deps, {
|
|
39686
40090
|
refresh: opts.refresh,
|
|
39687
40091
|
schemas: true,
|
|
39688
|
-
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
40092
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
40093
|
+
searchSync: opts.searchSync
|
|
39689
40094
|
});
|
|
39690
40095
|
const cmd = full.cmds.find((c) => c.name === command);
|
|
39691
40096
|
if (cmd === void 0) return null;
|
|
@@ -39711,6 +40116,7 @@ var mcpCommand = (nodePath, nodeDescription, command) => {
|
|
|
39711
40116
|
invokePath,
|
|
39712
40117
|
description: command.h ?? nodeDescription,
|
|
39713
40118
|
...command.inputSchema !== void 0 ? { inputSchema: command.inputSchema } : {},
|
|
40119
|
+
...command.delivery !== void 0 ? { delivery: command.delivery } : {},
|
|
39714
40120
|
...command.effect !== void 0 ? { effect: command.effect } : {},
|
|
39715
40121
|
...command.confirm === true ? { confirm: true } : {}
|
|
39716
40122
|
};
|
|
@@ -39725,24 +40131,13 @@ var toolSpecCommand = (node, tool, providerBacked = false) => {
|
|
|
39725
40131
|
...providerBacked ? { providerBacked: true } : {},
|
|
39726
40132
|
description: tool.description ?? node.description,
|
|
39727
40133
|
...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
|
|
40134
|
+
...tool.delivery !== void 0 ? { delivery: tool.delivery } : {},
|
|
39728
40135
|
...tool.effect !== void 0 ? { effect: tool.effect } : {},
|
|
39729
40136
|
...tool.confirm === true ? { confirm: true } : {}
|
|
39730
40137
|
};
|
|
39731
40138
|
};
|
|
39732
|
-
var remoteCommand = (localNodePath, model, command) => {
|
|
39733
|
-
const remoteNodePath = model.node.path.replace(/^\/+|\/+$/g, "");
|
|
39734
|
-
const remoteCommandPath = command.path.replace(/^\/+|\/+$/g, "");
|
|
39735
|
-
if (remoteCommandPath !== remoteNodePath && !remoteCommandPath.startsWith(`${remoteNodePath}/`)) {
|
|
39736
|
-
throw new TBError("unavailable", "remote ~help returned a command outside its node");
|
|
39737
|
-
}
|
|
39738
|
-
const suffix = remoteCommandPath.slice(remoteNodePath.length);
|
|
39739
|
-
return mcpCommand(localNodePath, model.node.description, {
|
|
39740
|
-
...command,
|
|
39741
|
-
path: `/${localNodePath}${suffix}`
|
|
39742
|
-
});
|
|
39743
|
-
};
|
|
39744
40139
|
function mcpBridgeFor(c, env, app) {
|
|
39745
|
-
const { builtinsOf, deps, globalSearchCapabilities } = env;
|
|
40140
|
+
const { builtinsOf, deps, globalSearchCapabilities, searchSync } = env;
|
|
39746
40141
|
const ctx = c.get("ctx");
|
|
39747
40142
|
const registry = new NodeRegistryStore(c.get("store"));
|
|
39748
40143
|
let remoteRequests = 0;
|
|
@@ -39761,7 +40156,7 @@ function mcpBridgeFor(c, env, app) {
|
|
|
39761
40156
|
additionalProperties: false,
|
|
39762
40157
|
properties: {
|
|
39763
40158
|
query: { type: "string", minLength: 1 },
|
|
39764
|
-
mode: { type: "string", enum: ["keyword"
|
|
40159
|
+
mode: { type: "string", enum: ["keyword"] },
|
|
39765
40160
|
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
39766
40161
|
cursor: { type: "string", minLength: 1 },
|
|
39767
40162
|
detail: { type: "string", enum: ["compact", "full"], default: "compact" },
|
|
@@ -39829,6 +40224,10 @@ function mcpBridgeFor(c, env, app) {
|
|
|
39829
40224
|
};
|
|
39830
40225
|
const remoteHelp = async (path) => {
|
|
39831
40226
|
takeRemoteRequest();
|
|
40227
|
+
const projector = await remotePathProjectorIfMatch(registry, path);
|
|
40228
|
+
if (projector === null) {
|
|
40229
|
+
throw remoteProtocolError(`remote ~help path '${path}' lost its mount owner`);
|
|
40230
|
+
}
|
|
39832
40231
|
const headers = new Headers(c.req.raw.headers);
|
|
39833
40232
|
headers.set("accept", "application/json");
|
|
39834
40233
|
const response = await remotePassthroughIfMatch(
|
|
@@ -39843,28 +40242,11 @@ function mcpBridgeFor(c, env, app) {
|
|
|
39843
40242
|
if (response === null || !response.ok) {
|
|
39844
40243
|
throw new TBError("unavailable", `remote ~help failed for '${path}'`, { retryable: true });
|
|
39845
40244
|
}
|
|
39846
|
-
const
|
|
39847
|
-
if (
|
|
40245
|
+
const parsed = helpJsonSchema.safeParse(await response.json().catch(() => null));
|
|
40246
|
+
if (!parsed.success) {
|
|
39848
40247
|
throw new TBError("unavailable", `remote ~help returned invalid JSON for '${path}'`);
|
|
39849
40248
|
}
|
|
39850
|
-
|
|
39851
|
-
if (owner?.node.kind !== "remote") {
|
|
39852
|
-
throw remoteProtocolError(`remote ~help path '${path}' lost its mount owner`);
|
|
39853
|
-
}
|
|
39854
|
-
const modelPath = canonicalRemotePath(model.node.path, true);
|
|
39855
|
-
if (modelPath !== owner.rest) {
|
|
39856
|
-
throw remoteProtocolError(`remote ~help path '${modelPath}' does not match request`);
|
|
39857
|
-
}
|
|
39858
|
-
for (const command of model.cmds) {
|
|
39859
|
-
if (!command.path.startsWith("/") || command.path.startsWith("//")) {
|
|
39860
|
-
throw remoteProtocolError(`remote ~help returned invalid command path '${command.path}'`);
|
|
39861
|
-
}
|
|
39862
|
-
const commandPath = canonicalRemotePath(command.path.slice(1), true);
|
|
39863
|
-
if (!remotePathWithin(modelPath, commandPath)) {
|
|
39864
|
-
throw remoteProtocolError(`remote ~help command '${command.path}' escapes its node`);
|
|
39865
|
-
}
|
|
39866
|
-
}
|
|
39867
|
-
return model;
|
|
40249
|
+
return projector.projectHelp(parsed.data, path, ctx);
|
|
39868
40250
|
};
|
|
39869
40251
|
const remotePaths = async (root) => {
|
|
39870
40252
|
const found = [];
|
|
@@ -39913,13 +40295,10 @@ function mcpBridgeFor(c, env, app) {
|
|
|
39913
40295
|
if (check(ctx, path, command.scope).allow) {
|
|
39914
40296
|
let detailed = command;
|
|
39915
40297
|
if (command.inputSchema === void 0 && command.path !== `/${model2.node.path}`) {
|
|
39916
|
-
const
|
|
39917
|
-
const remoteCommandPath = command.path.replace(/^\/+|\/+$/g, "");
|
|
39918
|
-
const detailPath = `${path}${remoteCommandPath.slice(remoteNodePath.length)}`;
|
|
39919
|
-
const detail = await remoteHelp(detailPath);
|
|
40298
|
+
const detail = await remoteHelp(command.path.replace(/^\/+/, ""));
|
|
39920
40299
|
detailed = detail.cmds.find((item) => item.name === command.name) ?? command;
|
|
39921
40300
|
}
|
|
39922
|
-
result.push(
|
|
40301
|
+
result.push(mcpCommand(path, model2.node.description, detailed));
|
|
39923
40302
|
}
|
|
39924
40303
|
}
|
|
39925
40304
|
}
|
|
@@ -39935,7 +40314,7 @@ function mcpBridgeFor(c, env, app) {
|
|
|
39935
40314
|
if ((node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config !== void 0) {
|
|
39936
40315
|
if (!check(ctx, node.path, "call").allow) continue;
|
|
39937
40316
|
const provider = await providerFor(node, ctx, deps);
|
|
39938
|
-
const raw = await upstreamTools(node, provider, deps, false, now);
|
|
40317
|
+
const raw = await upstreamTools(node, provider, deps, false, now, searchSync);
|
|
39939
40318
|
const { exposed } = virtualizeTools(node.virtualize, raw);
|
|
39940
40319
|
result.push(...exposed.map((tool) => toolSpecCommand(node, tool, true)));
|
|
39941
40320
|
continue;
|
|
@@ -39943,7 +40322,8 @@ function mcpBridgeFor(c, env, app) {
|
|
|
39943
40322
|
const model = await helpModelFor(node, registry, ctx, builtinsOf(c.get("store")), deps, {
|
|
39944
40323
|
includeDirectUpload: check(ctx, node.path, "write").allow,
|
|
39945
40324
|
refresh: false,
|
|
39946
|
-
now
|
|
40325
|
+
now,
|
|
40326
|
+
searchSync
|
|
39947
40327
|
});
|
|
39948
40328
|
for (const command of model.cmds) {
|
|
39949
40329
|
if (check(ctx, node.path, command.scope).allow) {
|
|
@@ -40046,13 +40426,13 @@ function mcpBridgeFor(c, env, app) {
|
|
|
40046
40426
|
throw TBError.notFound("not found");
|
|
40047
40427
|
}
|
|
40048
40428
|
const provider = await providerFor(node, ctx, deps);
|
|
40049
|
-
const raw = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString());
|
|
40429
|
+
const raw = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
|
|
40050
40430
|
const upstreamName = resolveUpstreamTool(node.virtualize, raw, tool.toolName);
|
|
40051
|
-
const
|
|
40431
|
+
const result2 = await provider.call(upstreamName, args);
|
|
40052
40432
|
return {
|
|
40053
|
-
content:
|
|
40054
|
-
...
|
|
40055
|
-
...
|
|
40433
|
+
content: result2.contentBlocks ?? result2.content,
|
|
40434
|
+
...result2.isError === true ? { isError: true } : {},
|
|
40435
|
+
...result2.structuredContent !== void 0 ? { structuredContent: result2.structuredContent } : {}
|
|
40056
40436
|
};
|
|
40057
40437
|
}
|
|
40058
40438
|
const url2 = new URL(tool.invokePath, c.req.url);
|
|
@@ -40064,7 +40444,13 @@ function mcpBridgeFor(c, env, app) {
|
|
|
40064
40444
|
const response = await app.request(
|
|
40065
40445
|
new Request(url2, { method: "POST", headers, body: JSON.stringify(args) })
|
|
40066
40446
|
);
|
|
40067
|
-
|
|
40447
|
+
const result = await resultFromResponse(response);
|
|
40448
|
+
if (args["~delivery"] !== void 0 && response.ok) {
|
|
40449
|
+
return {
|
|
40450
|
+
content: response.status === 202 ? { delivery: "mailbox", operation: result.content } : { delivery: "realtime", result: result.content }
|
|
40451
|
+
};
|
|
40452
|
+
}
|
|
40453
|
+
return result;
|
|
40068
40454
|
}
|
|
40069
40455
|
};
|
|
40070
40456
|
}
|
|
@@ -40099,26 +40485,102 @@ async function handleInvoke(c, env) {
|
|
|
40099
40485
|
const node = resolved.node;
|
|
40100
40486
|
const command = resolved.rest;
|
|
40101
40487
|
if (!check(ctx, node.path, "read").allow) throw TBError.notFound("not found");
|
|
40488
|
+
const toolMarker = deviceToolMarker(node);
|
|
40489
|
+
let invokeRequest;
|
|
40490
|
+
const readInvokeRequest = async () => {
|
|
40491
|
+
if (invokeRequest !== void 0) return await invokeRequest;
|
|
40492
|
+
invokeRequest = (async () => {
|
|
40493
|
+
const parsed = toolMarker === null ? await c.req.json().catch(() => null) : await mailboxJsonObject(c, { allowEmpty: true });
|
|
40494
|
+
if (parsed !== null && (typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
40495
|
+
throw new TBError("invalid_argument", "body must be a JSON object (command arguments)");
|
|
40496
|
+
}
|
|
40497
|
+
const raw2 = { ...parsed ?? {} };
|
|
40498
|
+
const unknownControls = Object.keys(raw2).filter(
|
|
40499
|
+
(key) => key.startsWith("~") && key !== "~delivery"
|
|
40500
|
+
);
|
|
40501
|
+
if (unknownControls.length > 0) {
|
|
40502
|
+
throw new TBError(
|
|
40503
|
+
"invalid_argument",
|
|
40504
|
+
`unknown invocation control '${unknownControls[0]}'`
|
|
40505
|
+
);
|
|
40506
|
+
}
|
|
40507
|
+
const rawDelivery = raw2["~delivery"];
|
|
40508
|
+
delete raw2["~delivery"];
|
|
40509
|
+
if (rawDelivery !== void 0 && rawDelivery !== "realtime" && rawDelivery !== "mailbox" && rawDelivery !== "fallback") {
|
|
40510
|
+
throw new TBError(
|
|
40511
|
+
"invalid_argument",
|
|
40512
|
+
"'~delivery' must be one of realtime, mailbox, fallback"
|
|
40513
|
+
);
|
|
40514
|
+
}
|
|
40515
|
+
return {
|
|
40516
|
+
arguments: raw2,
|
|
40517
|
+
delivery: rawDelivery ?? "realtime",
|
|
40518
|
+
explicitDelivery: rawDelivery !== void 0
|
|
40519
|
+
};
|
|
40520
|
+
})();
|
|
40521
|
+
return await invokeRequest;
|
|
40522
|
+
};
|
|
40102
40523
|
const readInvokeBody = async () => {
|
|
40103
|
-
const
|
|
40104
|
-
if (
|
|
40105
|
-
throw new TBError(
|
|
40524
|
+
const request = await readInvokeRequest();
|
|
40525
|
+
if (request.explicitDelivery) {
|
|
40526
|
+
throw new TBError(
|
|
40527
|
+
"invalid_argument",
|
|
40528
|
+
"'~delivery' is only supported by device-backed tool commands"
|
|
40529
|
+
);
|
|
40106
40530
|
}
|
|
40107
|
-
return
|
|
40531
|
+
return request.arguments;
|
|
40108
40532
|
};
|
|
40109
|
-
const toolMarker = deviceToolMarker(node);
|
|
40110
40533
|
if (toolMarker !== null) {
|
|
40111
40534
|
if (!check(ctx, node.path, "call").allow) {
|
|
40112
40535
|
throw new TBError("permission_denied", `no scope grants 'call' on '${node.path}'`);
|
|
40113
40536
|
}
|
|
40114
|
-
const
|
|
40115
|
-
|
|
40537
|
+
const declared = toolMarker.cmds?.find((item) => item.name.toLowerCase() === command);
|
|
40538
|
+
if (toolMarker.cmds !== void 0 && declared === void 0) throw TBError.notFound("not found");
|
|
40539
|
+
const request = await readInvokeRequest();
|
|
40540
|
+
const capability = declared?.delivery ?? "realtime";
|
|
40541
|
+
const canRealtime = capability === "realtime" || capability === "both";
|
|
40542
|
+
const canMailbox = capability === "mailbox" || capability === "both";
|
|
40543
|
+
if (request.delivery === "realtime" && !canRealtime) {
|
|
40544
|
+
throw new TBError("invalid_argument", "device command does not support realtime delivery");
|
|
40545
|
+
}
|
|
40546
|
+
if (request.delivery === "mailbox" && !canMailbox) {
|
|
40547
|
+
throw new TBError("invalid_argument", "device command does not support mailbox delivery");
|
|
40548
|
+
}
|
|
40549
|
+
if (request.delivery === "fallback" && !canMailbox) {
|
|
40550
|
+
throw new TBError("invalid_argument", "device command does not support mailbox fallback");
|
|
40551
|
+
}
|
|
40552
|
+
const ttlSeconds = request.delivery === "realtime" ? void 0 : deviceOperationTtlSeconds(new URL(c.req.url));
|
|
40553
|
+
const enqueue = async () => await enqueueDeviceCommand(c, env, {
|
|
40554
|
+
arguments: request.arguments,
|
|
40555
|
+
command,
|
|
40556
|
+
marker: toolMarker,
|
|
40557
|
+
node,
|
|
40558
|
+
...ttlSeconds === void 0 ? {} : { ttlSeconds }
|
|
40559
|
+
});
|
|
40560
|
+
if (request.delivery === "mailbox" || !canRealtime) return await enqueue();
|
|
40561
|
+
if (request.delivery === "fallback" && deps.device === void 0) return await enqueue();
|
|
40562
|
+
const attempt2 = await attemptDevice(deps, toolMarker.deviceId, {
|
|
40116
40563
|
// 帧 path 含命令叶子段:<mount 相对路径>/<命令>。
|
|
40117
40564
|
path: `${relativeDevicePath(node.path, toolMarker.mountPath)}/${command}`,
|
|
40118
|
-
arguments:
|
|
40565
|
+
arguments: request.arguments,
|
|
40119
40566
|
context: deviceCallContextFrom(ctx)
|
|
40120
40567
|
});
|
|
40121
|
-
|
|
40568
|
+
if (attempt2.disposition === "not_dispatched" && request.delivery === "fallback") {
|
|
40569
|
+
return await enqueue();
|
|
40570
|
+
}
|
|
40571
|
+
if (attempt2.result.ok) {
|
|
40572
|
+
const response = renderResult(attempt2.result.value, negotiate(c.req.header("accept")));
|
|
40573
|
+
response.headers.set("x-tb-delivery", "realtime");
|
|
40574
|
+
return response;
|
|
40575
|
+
}
|
|
40576
|
+
if (attempt2.disposition === "unknown" && request.delivery === "fallback") {
|
|
40577
|
+
throw new TBError(
|
|
40578
|
+
attempt2.result.error.code,
|
|
40579
|
+
`${attempt2.result.error.message}; delivery outcome is unknown and was not enqueued`,
|
|
40580
|
+
{ retryable: false }
|
|
40581
|
+
);
|
|
40582
|
+
}
|
|
40583
|
+
throw tbErrorFromBody(attempt2.result.error);
|
|
40122
40584
|
}
|
|
40123
40585
|
if ((node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config !== void 0) {
|
|
40124
40586
|
if (!check(ctx, node.path, "call").allow) {
|
|
@@ -40126,7 +40588,7 @@ async function handleInvoke(c, env) {
|
|
|
40126
40588
|
}
|
|
40127
40589
|
const args2 = await readInvokeBody();
|
|
40128
40590
|
const provider = await providerFor(node, ctx, deps);
|
|
40129
|
-
const tools = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString());
|
|
40591
|
+
const tools = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
|
|
40130
40592
|
const upstreamName = resolveUpstreamTool(node.virtualize, tools, command);
|
|
40131
40593
|
const result2 = await provider.call(upstreamName, args2);
|
|
40132
40594
|
return renderResult(result2.content, negotiate(c.req.header("accept")));
|
|
@@ -40190,7 +40652,7 @@ async function handleInvoke(c, env) {
|
|
|
40190
40652
|
if (cfg.provider !== "r2" && cfg.provider !== "s3") {
|
|
40191
40653
|
const local = localContext(deps, node);
|
|
40192
40654
|
if (local !== null) {
|
|
40193
|
-
const result4 = await
|
|
40655
|
+
const result4 = await dispatchContextCmd(local, command, args2);
|
|
40194
40656
|
return renderResult(result4, negotiate(c.req.header("accept")));
|
|
40195
40657
|
}
|
|
40196
40658
|
const { manifest, export: exported } = await requirePluginExport(
|
|
@@ -40210,11 +40672,11 @@ async function handleInvoke(c, env) {
|
|
|
40210
40672
|
...cfg.authRef !== void 0 ? { upstreamAuthRef: cfg.authRef } : {},
|
|
40211
40673
|
...deps.pluginBindings !== void 0 ? { bindings: deps.pluginBindings } : {}
|
|
40212
40674
|
});
|
|
40213
|
-
const result3 = await
|
|
40675
|
+
const result3 = await dispatchContextCmd(provider2, command, args2);
|
|
40214
40676
|
return renderResult(result3, negotiate(c.req.header("accept")));
|
|
40215
40677
|
}
|
|
40216
40678
|
const provider = await contextProviderFor(node, cfg, deps, c.req.url);
|
|
40217
|
-
const result2 = await
|
|
40679
|
+
const result2 = await dispatchContextCmd(provider, command, args2);
|
|
40218
40680
|
return renderResult(result2, negotiate(c.req.header("accept")));
|
|
40219
40681
|
}
|
|
40220
40682
|
if (node.kind === "skillhub" && node.config?.kind === "skillhub") {
|
|
@@ -40232,7 +40694,7 @@ async function handleInvoke(c, env) {
|
|
|
40232
40694
|
throw new TBError("permission_denied", `readOnly \u6302\u8F7D\u62D2\u7EDD '${command}'`);
|
|
40233
40695
|
}
|
|
40234
40696
|
const provider = await skillhubProviderFor(node, cfg, deps, c.req.url);
|
|
40235
|
-
const result2 = await
|
|
40697
|
+
const result2 = await dispatchSkillhubCmd(provider, command, args2);
|
|
40236
40698
|
return renderResult(result2, negotiate(c.req.header("accept")));
|
|
40237
40699
|
}
|
|
40238
40700
|
if (node.kind !== "builtin" || node.config?.kind !== "builtin") {
|
|
@@ -40256,19 +40718,14 @@ async function handleInvoke(c, env) {
|
|
|
40256
40718
|
throw new TBError("invalid_argument", "field 'path' must be a string");
|
|
40257
40719
|
}
|
|
40258
40720
|
const cfgPatch = cmd === "write" ? args.config : cmd === "update" ? args.patch?.config : void 0;
|
|
40259
|
-
|
|
40260
|
-
|
|
40261
|
-
|
|
40721
|
+
await assertNodeConfigMutation({
|
|
40722
|
+
action: cmd === "delete" ? "delete" : "write",
|
|
40723
|
+
config: cfgPatch,
|
|
40262
40724
|
ctx,
|
|
40263
|
-
|
|
40264
|
-
|
|
40265
|
-
|
|
40266
|
-
);
|
|
40267
|
-
assertSecretRefUse(ctx.scopes, cfgPatch);
|
|
40268
|
-
await assertMcpOAuthConfig(cfgPatch, deps.secrets);
|
|
40269
|
-
await assertContextConfig(cfgPatch, deps);
|
|
40270
|
-
await assertSkillhubConfig(cfgPatch, deps);
|
|
40271
|
-
await assertToolConfig(cfgPatch, deps, ctx, targetPath);
|
|
40725
|
+
deps,
|
|
40726
|
+
registry,
|
|
40727
|
+
targetPath
|
|
40728
|
+
});
|
|
40272
40729
|
registryTarget = targetPath;
|
|
40273
40730
|
}
|
|
40274
40731
|
if (registryTarget !== void 0 && (cmd === "write" || cmd === "update")) {
|
|
@@ -40298,22 +40755,16 @@ async function handleInvoke(c, env) {
|
|
|
40298
40755
|
return tbErrorResponse(new TBError("internal", "internal error"));
|
|
40299
40756
|
}
|
|
40300
40757
|
if (registryTarget !== void 0) {
|
|
40301
|
-
await
|
|
40302
|
-
await invalidateMcpEra(store, registryTarget);
|
|
40303
|
-
await invalidateMcpOAuth(store, registryTarget);
|
|
40304
|
-
await invalidateProviderOAuth(store, registryTarget);
|
|
40758
|
+
await invalidateNodeDerivedState(store, registryTarget);
|
|
40305
40759
|
await searchSync?.reconcileNodeQuietly(registryTarget, { marker: registryMarker });
|
|
40306
40760
|
const current = await registry.get(registryTarget).catch(() => null);
|
|
40307
|
-
if (current !== null && await refreshDynamicSearchNode(current, ctx, deps)) {
|
|
40761
|
+
if (current !== null && await refreshDynamicSearchNode(current, ctx, deps, searchSync)) {
|
|
40308
40762
|
await searchSync?.abort(registryMarker);
|
|
40309
40763
|
}
|
|
40310
40764
|
}
|
|
40311
40765
|
const pluginEmptyPaths = [];
|
|
40312
40766
|
for (const mount of pluginMounts) {
|
|
40313
|
-
await
|
|
40314
|
-
await invalidateMcpEra(store, mount.node.path);
|
|
40315
|
-
await invalidateMcpOAuth(store, mount.node.path);
|
|
40316
|
-
await invalidateProviderOAuth(store, mount.node.path);
|
|
40767
|
+
await invalidateNodeDerivedState(store, mount.node.path);
|
|
40317
40768
|
const providerId = mount.node.config?.kind === "tool" ? mount.node.config.provider : "";
|
|
40318
40769
|
const manifest = await store.get(KEY_PLUGIN + providerId);
|
|
40319
40770
|
if (manifest !== null && manifest.enabled !== false) {
|
|
@@ -40361,8 +40812,7 @@ function createRouteEnv(deps) {
|
|
|
40361
40812
|
if (!declared.has("search")) return [];
|
|
40362
40813
|
return [
|
|
40363
40814
|
"search",
|
|
40364
|
-
...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : []
|
|
40365
|
-
...declared.has("search:semantic") ? ["search:semantic"] : []
|
|
40815
|
+
...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : []
|
|
40366
40816
|
];
|
|
40367
40817
|
};
|
|
40368
40818
|
let catalogDigest;
|
|
@@ -40372,12 +40822,17 @@ function createRouteEnv(deps) {
|
|
|
40372
40822
|
catalogDigest ??= catalogSetDigest(catalog);
|
|
40373
40823
|
return catalogDigest;
|
|
40374
40824
|
};
|
|
40375
|
-
|
|
40825
|
+
let mailboxInstance;
|
|
40826
|
+
const mailbox = () => {
|
|
40827
|
+
mailboxInstance ??= createDeviceMailboxService(deps);
|
|
40828
|
+
return mailboxInstance;
|
|
40829
|
+
};
|
|
40830
|
+
return { builtinsOf, deps, globalSearchCapabilities, mailbox, pluginCatalogDigest, searchSync };
|
|
40376
40831
|
}
|
|
40377
40832
|
|
|
40378
40833
|
// ../app/src/routes/help.ts
|
|
40379
40834
|
async function handleHelp(c, env) {
|
|
40380
|
-
const { builtinsOf, deps } = env;
|
|
40835
|
+
const { builtinsOf, deps, searchSync } = env;
|
|
40381
40836
|
const path = splitReserved(new URL(c.req.url).pathname, "~help");
|
|
40382
40837
|
if (path === null) throw TBError.notFound("no such path");
|
|
40383
40838
|
const ctx = c.get("ctx");
|
|
@@ -40416,11 +40871,12 @@ async function handleHelp(c, env) {
|
|
|
40416
40871
|
try {
|
|
40417
40872
|
node = await registry.get(path);
|
|
40418
40873
|
} catch {
|
|
40419
|
-
const toolModel = await toolHelpModelFor(c, ctx, registry, path, deps);
|
|
40874
|
+
const toolModel = await toolHelpModelFor(c, ctx, registry, path, deps, searchSync);
|
|
40420
40875
|
if (toolModel !== null) return renderHelp(await enrichHelp(toolModel, path, store), rep);
|
|
40421
40876
|
const cmdModel = await commandHelpModelFor(registry, ctx, builtins, deps, path, {
|
|
40422
40877
|
refresh,
|
|
40423
|
-
schemas: schemas2
|
|
40878
|
+
schemas: schemas2,
|
|
40879
|
+
searchSync
|
|
40424
40880
|
});
|
|
40425
40881
|
if (cmdModel !== null) return renderHelp(await enrichHelp(cmdModel, path, store), rep);
|
|
40426
40882
|
throw TBError.notFound("not found");
|
|
@@ -40428,7 +40884,8 @@ async function handleHelp(c, env) {
|
|
|
40428
40884
|
const model = await helpModelFor(node, registry, ctx, builtins, deps, {
|
|
40429
40885
|
refresh,
|
|
40430
40886
|
schemas: schemas2,
|
|
40431
|
-
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
40887
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
40888
|
+
searchSync
|
|
40432
40889
|
});
|
|
40433
40890
|
return renderHelp(await enrichHelp(model, path, store), rep);
|
|
40434
40891
|
}
|
|
@@ -40536,6 +40993,7 @@ function createTbApp(deps) {
|
|
|
40536
40993
|
});
|
|
40537
40994
|
registerMcpRoute(app, env);
|
|
40538
40995
|
registerSearchRoute(app, env);
|
|
40996
|
+
registerDeviceMailboxRoutes(app, env);
|
|
40539
40997
|
app.get(
|
|
40540
40998
|
"/system/device/ws",
|
|
40541
40999
|
(c) => runHandler(async () => {
|
|
@@ -40666,7 +41124,10 @@ var DeviceHub = class {
|
|
|
40666
41124
|
*/
|
|
40667
41125
|
async invoke(deviceId, req) {
|
|
40668
41126
|
if (this.activeByDevice.has(deviceId)) return await this.invokeLocal(deviceId, req);
|
|
40669
|
-
const offline = {
|
|
41127
|
+
const offline = {
|
|
41128
|
+
disposition: "not_dispatched",
|
|
41129
|
+
result: { ok: false, error: TBError.deviceOffline().toJSON() }
|
|
41130
|
+
};
|
|
40670
41131
|
if (this.router === void 0) return offline;
|
|
40671
41132
|
return await this.router.forward(deviceId, req) ?? offline;
|
|
40672
41133
|
}
|
|
@@ -40677,26 +41138,27 @@ var DeviceHub = class {
|
|
|
40677
41138
|
* 那会再次查路由并转发,在两个副本间弹来弹去。这里没有连接就直接 offline。
|
|
40678
41139
|
*/
|
|
40679
41140
|
async invokeLocal(deviceId, req) {
|
|
40680
|
-
const offline = {
|
|
41141
|
+
const offline = {
|
|
41142
|
+
disposition: "not_dispatched",
|
|
41143
|
+
result: { ok: false, error: TBError.deviceOffline().toJSON() }
|
|
41144
|
+
};
|
|
40681
41145
|
const conn = this.activeByDevice.get(deviceId);
|
|
40682
41146
|
if (conn === void 0) return offline;
|
|
40683
|
-
const
|
|
40684
|
-
|
|
40685
|
-
|
|
40686
|
-
|
|
40687
|
-
|
|
40688
|
-
|
|
40689
|
-
},
|
|
40690
|
-
targetPath: conn.mountPath,
|
|
40691
|
-
action: "write",
|
|
40692
|
-
existing: null
|
|
40693
|
-
}).allow;
|
|
41147
|
+
const authorized = conn.keyId !== void 0 && conn.mountPath !== void 0 && await reverifyDeviceAuthority({
|
|
41148
|
+
store: this.store,
|
|
41149
|
+
authorization: conn.authorization,
|
|
41150
|
+
keyId: conn.keyId,
|
|
41151
|
+
mountPath: conn.mountPath
|
|
41152
|
+
});
|
|
40694
41153
|
if (!authorized) {
|
|
40695
41154
|
conn.session.reject(TBError.unauthenticated());
|
|
40696
41155
|
return offline;
|
|
40697
41156
|
}
|
|
40698
41157
|
if (this.activeByDevice.get(deviceId) !== conn) return offline;
|
|
40699
|
-
return await new Promise((resolve3) => conn.session.call(
|
|
41158
|
+
return await new Promise((resolve3) => conn.session.call(
|
|
41159
|
+
req,
|
|
41160
|
+
(result, disposition) => resolve3({ disposition, result })
|
|
41161
|
+
));
|
|
40700
41162
|
}
|
|
40701
41163
|
/** DeviceChannel.ws:Node 宿主的升级在 http 层处理,永不应命中此路由。 */
|
|
40702
41164
|
async ws() {
|
|
@@ -40719,10 +41181,11 @@ var DeviceHub = class {
|
|
|
40719
41181
|
}
|
|
40720
41182
|
if (meta.disconnectedAt === void 0) {
|
|
40721
41183
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
40722
|
-
|
|
40723
|
-
|
|
40724
|
-
|
|
40725
|
-
|
|
41184
|
+
await markDeviceDisconnected({
|
|
41185
|
+
mountPath: meta.mountPath,
|
|
41186
|
+
now,
|
|
41187
|
+
registry: new NodeRegistryStore(this.store)
|
|
41188
|
+
});
|
|
40726
41189
|
await this.store.put(KEY_DEVICE_META + meta.deviceId, { ...meta, disconnectedAt: now });
|
|
40727
41190
|
this.scheduleReclaim(meta.deviceId, this.reclaimSec * 1e3);
|
|
40728
41191
|
} else {
|
|
@@ -40820,9 +41283,7 @@ var DeviceHub = class {
|
|
|
40820
41283
|
...this.search === void 0 ? {} : { search: this.search }
|
|
40821
41284
|
});
|
|
40822
41285
|
if (!searchIndexed) {
|
|
40823
|
-
console.warn(
|
|
40824
|
-
`[tool-bridge] device '${conn.deviceId}' mounted at ${mountPath} but its tools are NOT in the search index: registry exceeds the tool-search capacity (TOOL_SEARCH_AUDIT_NODE_LIMIT). The device is callable but won't appear in \`tb search\` until capacity frees up.`
|
|
40825
|
-
);
|
|
41286
|
+
console.warn(deviceSearchCapacityWarning(conn.deviceId, mountPath));
|
|
40826
41287
|
}
|
|
40827
41288
|
const meta = { deviceId: conn.deviceId, mountPath, keyId, connectedAt: now };
|
|
40828
41289
|
await this.store.put(KEY_DEVICE_META + conn.deviceId, meta);
|
|
@@ -40847,10 +41308,11 @@ var DeviceHub = class {
|
|
|
40847
41308
|
const raw = await this.store.get(KEY_DEVICE_META + conn.deviceId);
|
|
40848
41309
|
if (!isDeviceMeta(raw)) return;
|
|
40849
41310
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
40853
|
-
|
|
41311
|
+
await markDeviceDisconnected({
|
|
41312
|
+
mountPath: raw.mountPath,
|
|
41313
|
+
now,
|
|
41314
|
+
registry: new NodeRegistryStore(this.store)
|
|
41315
|
+
});
|
|
40854
41316
|
await this.store.put(KEY_DEVICE_META + conn.deviceId, { ...raw, disconnectedAt: now });
|
|
40855
41317
|
this.scheduleReclaim(conn.deviceId, this.reclaimSec * 1e3);
|
|
40856
41318
|
}
|
|
@@ -40883,13 +41345,12 @@ var DeviceHub = class {
|
|
|
40883
41345
|
}
|
|
40884
41346
|
const raw = await this.store.get(KEY_DEVICE_META + deviceId);
|
|
40885
41347
|
if (!isDeviceMeta(raw)) return;
|
|
40886
|
-
|
|
40887
|
-
|
|
40888
|
-
|
|
40889
|
-
|
|
40890
|
-
|
|
40891
|
-
}
|
|
40892
|
-
await searchSync?.removeSubtreeQuietly(raw.mountPath, marker);
|
|
41348
|
+
await reclaimDeviceSubtree({
|
|
41349
|
+
mountPath: raw.mountPath,
|
|
41350
|
+
registry: new NodeRegistryStore(this.store),
|
|
41351
|
+
...this.search === void 0 ? {} : { search: this.search },
|
|
41352
|
+
state: this.store
|
|
41353
|
+
});
|
|
40893
41354
|
await this.store.delete(KEY_DEVICE_META + deviceId);
|
|
40894
41355
|
}
|
|
40895
41356
|
/** ws 协议层探活:上一轮未回 pong → terminate(触发 close → markDisconnected)。 */
|
|
@@ -40973,7 +41434,13 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
40973
41434
|
return await new Promise((resolve3) => {
|
|
40974
41435
|
const timer = setTimeout(() => {
|
|
40975
41436
|
this.pending.delete(correlationId);
|
|
40976
|
-
resolve3({
|
|
41437
|
+
resolve3({
|
|
41438
|
+
disposition: "unknown",
|
|
41439
|
+
result: {
|
|
41440
|
+
ok: false,
|
|
41441
|
+
error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u8D85\u65F6").toJSON()
|
|
41442
|
+
}
|
|
41443
|
+
});
|
|
40977
41444
|
}, this.opts.forwardTimeoutMs ?? DEVICE_FORWARD_TIMEOUT_MS);
|
|
40978
41445
|
timer.unref?.();
|
|
40979
41446
|
this.pending.set(correlationId, { resolve: resolve3, timer });
|
|
@@ -40983,8 +41450,11 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
40983
41450
|
this.pending.delete(correlationId);
|
|
40984
41451
|
clearTimeout(waiter.timer);
|
|
40985
41452
|
waiter.resolve({
|
|
40986
|
-
|
|
40987
|
-
|
|
41453
|
+
disposition: "unknown",
|
|
41454
|
+
result: {
|
|
41455
|
+
ok: false,
|
|
41456
|
+
error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u5931\u8D25").toJSON()
|
|
41457
|
+
}
|
|
40988
41458
|
});
|
|
40989
41459
|
});
|
|
40990
41460
|
});
|
|
@@ -40994,7 +41464,13 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
40994
41464
|
this.renewTimer = void 0;
|
|
40995
41465
|
for (const [, waiter] of this.pending) {
|
|
40996
41466
|
clearTimeout(waiter.timer);
|
|
40997
|
-
waiter.resolve({
|
|
41467
|
+
waiter.resolve({
|
|
41468
|
+
disposition: "unknown",
|
|
41469
|
+
result: {
|
|
41470
|
+
ok: false,
|
|
41471
|
+
error: new TBError("unavailable", "\u7F51\u5173\u6B63\u5728\u5173\u95ED").toJSON()
|
|
41472
|
+
}
|
|
41473
|
+
});
|
|
40998
41474
|
}
|
|
40999
41475
|
this.pending.clear();
|
|
41000
41476
|
await Promise.all([...this.owned].map(async (id) => await this.release(id)));
|
|
@@ -41023,7 +41499,13 @@ var DeviceRouter = class _DeviceRouter {
|
|
|
41023
41499
|
try {
|
|
41024
41500
|
result = await this.opts.onLocalCall(call.deviceId, call.req);
|
|
41025
41501
|
} catch {
|
|
41026
|
-
result = {
|
|
41502
|
+
result = {
|
|
41503
|
+
disposition: "unknown",
|
|
41504
|
+
result: {
|
|
41505
|
+
ok: false,
|
|
41506
|
+
error: new TBError("internal", "\u8BBE\u5907\u8C03\u7528\u6267\u884C\u5931\u8D25").toJSON()
|
|
41507
|
+
}
|
|
41508
|
+
};
|
|
41027
41509
|
}
|
|
41028
41510
|
const reply = { correlationId: call.correlationId, result };
|
|
41029
41511
|
await this.backend.publish(deviceReplyChannel(call.replyTo), JSON.stringify(reply)).catch(() => {
|
|
@@ -41330,6 +41812,7 @@ var commandSchema = external_exports.strictObject({
|
|
|
41330
41812
|
argv: external_exports.array(external_exports.union([external_exports.string(), argumentSchema])).optional(),
|
|
41331
41813
|
cwd: external_exports.string().min(1).optional(),
|
|
41332
41814
|
effect: external_exports.enum(["read", "write", "destructive"]),
|
|
41815
|
+
delivery: external_exports.enum(["realtime", "mailbox", "both"]).optional(),
|
|
41333
41816
|
confirm: external_exports.boolean().optional(),
|
|
41334
41817
|
timeoutMs: external_exports.number().int().positive().max(SHELL_EXEC_DEFAULT_TIMEOUT_MS).optional(),
|
|
41335
41818
|
maxOutputBytes: external_exports.number().int().positive().max(STRUCTURED_COMMAND_MAX_OUTPUT_BYTES).optional(),
|
|
@@ -41958,7 +42441,7 @@ import { join as join4 } from "path";
|
|
|
41958
42441
|
// package.json
|
|
41959
42442
|
var package_default = {
|
|
41960
42443
|
name: "@tool-bridge/server",
|
|
41961
|
-
version: "0.
|
|
42444
|
+
version: "0.21.0",
|
|
41962
42445
|
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",
|
|
41963
42446
|
type: "module",
|
|
41964
42447
|
license: "MIT",
|
|
@@ -42297,26 +42780,27 @@ function createTbServer(config) {
|
|
|
42297
42780
|
}
|
|
42298
42781
|
const app = createTbApp(deps);
|
|
42299
42782
|
let server;
|
|
42300
|
-
let
|
|
42301
|
-
let
|
|
42302
|
-
const
|
|
42303
|
-
if (
|
|
42304
|
-
|
|
42783
|
+
let maintenanceTimer;
|
|
42784
|
+
let maintenanceInFlight = false;
|
|
42785
|
+
const runMaintenance = async () => {
|
|
42786
|
+
if (maintenanceInFlight) return;
|
|
42787
|
+
maintenanceInFlight = true;
|
|
42305
42788
|
try {
|
|
42306
42789
|
await cleanupDefaultStore(deps);
|
|
42790
|
+
if (deps.encryptionKey !== void 0) await cleanupDeviceMailbox(deps);
|
|
42307
42791
|
} finally {
|
|
42308
|
-
|
|
42792
|
+
maintenanceInFlight = false;
|
|
42309
42793
|
}
|
|
42310
42794
|
};
|
|
42311
42795
|
const reportStoreCleanupFailure = () => {
|
|
42312
42796
|
console.warn(JSON.stringify({ event: "tool_bridge_store_cleanup_failed" }));
|
|
42313
42797
|
};
|
|
42314
42798
|
const scheduleStoreCleanup = () => {
|
|
42315
|
-
void
|
|
42316
|
-
|
|
42317
|
-
void
|
|
42799
|
+
void runMaintenance().catch(reportStoreCleanupFailure);
|
|
42800
|
+
maintenanceTimer = setInterval(() => {
|
|
42801
|
+
void runMaintenance().catch(reportStoreCleanupFailure);
|
|
42318
42802
|
}, config.storeCleanupIntervalSec * 1e3);
|
|
42319
|
-
|
|
42803
|
+
maintenanceTimer.unref?.();
|
|
42320
42804
|
};
|
|
42321
42805
|
return {
|
|
42322
42806
|
app,
|
|
@@ -42345,9 +42829,9 @@ function createTbServer(config) {
|
|
|
42345
42829
|
},
|
|
42346
42830
|
async close() {
|
|
42347
42831
|
draining = true;
|
|
42348
|
-
if (
|
|
42349
|
-
clearInterval(
|
|
42350
|
-
|
|
42832
|
+
if (maintenanceTimer !== void 0) {
|
|
42833
|
+
clearInterval(maintenanceTimer);
|
|
42834
|
+
maintenanceTimer = void 0;
|
|
42351
42835
|
}
|
|
42352
42836
|
let closed;
|
|
42353
42837
|
if (server !== void 0) {
|