@tool-bridge/server 0.20.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.
Files changed (109) hide show
  1. package/dist/{airtable-ZNWHIJMO.js → airtable-DMJEEGJP.js} +19 -16
  2. package/dist/{alt_text_generator_ai-X7RMDDM5.js → alt_text_generator_ai-RELFQ5FV.js} +4 -4
  3. package/dist/{amap-3VER75VH.js → amap-YSYEACKQ.js} +4 -4
  4. package/dist/{anthropic-N7NTB3UC.js → anthropic-D5X5GSYE.js} +23 -25
  5. package/dist/{apify-JB5P5AJI.js → apify-5NVFU3IZ.js} +20 -19
  6. package/dist/{brave_search-USL5OOBS.js → brave_search-X557FMGO.js} +16 -14
  7. package/dist/chunk-JEWFX6Z6.js +71 -0
  8. package/dist/{chunk-RVUERWUZ.js → chunk-MDY7K5CX.js} +748 -788
  9. package/dist/{chunk-WIGQCP6O.js → chunk-MZCIPUNQ.js} +141 -176
  10. package/dist/{chunk-6X55ALDX.js → chunk-Q74GLIDK.js} +3 -3
  11. package/dist/{chunk-6ANLYNFB.js → chunk-QR4LK3HF.js} +1 -1
  12. package/dist/{chunk-3WL6EXYX.js → chunk-ZTTN6AJG.js} +1 -1
  13. package/dist/{circleci-LBGB25LP.js → circleci-3P3QTUX2.js} +20 -24
  14. package/dist/{clerk-6RX37CNZ.js → clerk-GNMZQ2MY.js} +22 -21
  15. package/dist/{cloudflare_dns-NQGW7FHV.js → cloudflare_dns-VWZH3ZTR.js} +21 -17
  16. package/dist/{cohere-XPC2NAEW.js → cohere-C7QVIIMG.js} +19 -17
  17. package/dist/{confluence-ZUG2LT5A.js → confluence-52I37IUT.js} +16 -11
  18. package/dist/{convertapi-ZLXKWV3L.js → convertapi-3TRJ3WEO.js} +3 -3
  19. package/dist/{currencyapi-T3A3HSOD.js → currencyapi-LWEANWVI.js} +23 -26
  20. package/dist/{deepseek-NJMOD23E.js → deepseek-5U24YG4P.js} +27 -17
  21. package/dist/{docker_hub-D2MQCSJ7.js → docker_hub-CMISLRSE.js} +4 -4
  22. package/dist/{dropbox-MI6U3SOT.js → dropbox-TW4QN6FT.js} +9 -8
  23. package/dist/{dub-P2BYBYV4.js → dub-EFXGI5UH.js} +22 -19
  24. package/dist/{e2b-BGKIAE54.js → e2b-PHM63OVF.js} +20 -20
  25. package/dist/{exa-EUY7VV4C.js → exa-DYOQWURR.js} +22 -25
  26. package/dist/{fal_ai-AAOG5FYJ.js → fal_ai-VJPMCNRH.js} +18 -17
  27. package/dist/{feishu-AN5IJYOW.js → feishu-TFIZXPIN.js} +2 -2
  28. package/dist/{feishu_custom_bot-BWGMNVXF.js → feishu_custom_bot-ADSE23KU.js} +3 -3
  29. package/dist/{figma-SJSWYCGD.js → figma-4AK47AYY.js} +21 -23
  30. package/dist/{firehydrant-MOXK2PST.js → firehydrant-RGPTW4EA.js} +15 -15
  31. package/dist/{fixer-ISIDUDZU.js → fixer-OAMF4DKP.js} +4 -4
  32. package/dist/{fly-U4KPSGBF.js → fly-YB4AQD4O.js} +20 -16
  33. package/dist/{front-ZHM2YX2E.js → front-6GTVYKUM.js} +20 -23
  34. package/dist/{geocodio-SVCCAXAV.js → geocodio-YRT4K25M.js} +4 -4
  35. package/dist/{ghost-INTR7FIY.js → ghost-JCW6UIP2.js} +4 -4
  36. package/dist/{github-T275VVWR.js → github-CEGVRMNX.js} +28 -27
  37. package/dist/{gmail-KSSPOE67.js → gmail-JCMJD55P.js} +17 -17
  38. package/dist/{googlecalendar-JB5RW27L.js → googlecalendar-2EAW4MNM.js} +16 -14
  39. package/dist/{googledocs-G522PUGW.js → googledocs-YNKKBM6Q.js} +3 -3
  40. package/dist/{grafana-44ZPIV5P.js → grafana-353VSGXN.js} +24 -25
  41. package/dist/{graphhopper-2MORXLOW.js → graphhopper-H3IVJNDS.js} +4 -4
  42. package/dist/index.d.ts +31 -30
  43. package/dist/index.js +3 -3
  44. package/dist/{ip2proxy-R4K4KEMI.js → ip2proxy-DBGRWH7S.js} +4 -4
  45. package/dist/{ipgeolocation_io-H4J55OGI.js → ipgeolocation_io-5I2BDVMG.js} +4 -4
  46. package/dist/{ipqualityscore-XGID4OHA.js → ipqualityscore-ILV7TQCV.js} +4 -4
  47. package/dist/{jina_ai-7ZVDOR3Z.js → jina_ai-L5BH455V.js} +20 -21
  48. package/dist/{jira-CTIYZTT3.js → jira-IGTILUZA.js} +20 -16
  49. package/dist/{langsmith-ISP567FS.js → langsmith-BYRX4AOF.js} +25 -25
  50. package/dist/{lemlist-OGKK272F.js → lemlist-ZGNYTIQO.js} +23 -24
  51. package/dist/{linear-6JX4E3JI.js → linear-HLGOWISN.js} +3 -3
  52. package/dist/{logsnag-BVB36ZCL.js → logsnag-5RGTMTU2.js} +20 -22
  53. package/dist/main.js +102 -102
  54. package/dist/{mattermost-UECVPZGY.js → mattermost-BONGXVC7.js} +21 -17
  55. package/dist/{mem0-EUA7S2OO.js → mem0-RUVNUPTE.js} +15 -15
  56. package/dist/{memos-BYSBJRWZ.js → memos-TED4HI54.js} +36 -32
  57. package/dist/{metabase-4JXZH3YG.js → metabase-ZHXHRHZK.js} +22 -18
  58. package/dist/{mistral_ai-2C5BTBKW.js → mistral_ai-NGP5SHU7.js} +24 -21
  59. package/dist/{mother_duck-ZGNNNR3X.js → mother_duck-TKVQM35M.js} +23 -19
  60. package/dist/{ngrok-NTPGMRXI.js → ngrok-BYCUJ7QV.js} +17 -22
  61. package/dist/{notes-KGYXX6N6.js → notes-KXRNNYDW.js} +1 -1
  62. package/dist/{notion-EX3X2G7G.js → notion-2ILYKJOO.js} +20 -17
  63. package/dist/{open_exchange_rates-2V2LIGPS.js → open_exchange_rates-JU4DC6HZ.js} +4 -4
  64. package/dist/{openai-KQFQPK7Z.js → openai-6SOT26ZK.js} +22 -16
  65. package/dist/{opengraph_io-B63SEYJY.js → opengraph_io-ZEFHV2OL.js} +4 -4
  66. package/dist/{openrouter-62A4H62Q.js → openrouter-R4P7JL5L.js} +19 -14
  67. package/dist/{outline-QC33XLCK.js → outline-VCTQ5V2X.js} +18 -20
  68. package/dist/{paddle-HDZG35Y7.js → paddle-AZOAKBJS.js} +17 -15
  69. package/dist/{perplexity-M7LOMJWA.js → perplexity-OMRLGVAT.js} +19 -23
  70. package/dist/{pinecone-Y7YCJRZM.js → pinecone-DI62W3ZX.js} +19 -24
  71. package/dist/{pipedrive-FLJA62UX.js → pipedrive-CW7HONYW.js} +22 -23
  72. package/dist/{polar-AE55Q425.js → polar-5DRGTXNR.js} +19 -17
  73. package/dist/{posthog-IAY5D3YW.js → posthog-3DTHFCKE.js} +29 -24
  74. package/dist/{postmark-NMKAXDXZ.js → postmark-ME2V2XXI.js} +19 -17
  75. package/dist/{prerender-CDJM7I7N.js → prerender-5EAY7GDM.js} +4 -4
  76. package/dist/{pubmed-N437ZJOR.js → pubmed-GRQUBGVA.js} +3 -3
  77. package/dist/{railway-NBWYXCUM.js → railway-FTM75TMX.js} +3 -3
  78. package/dist/{readwise-QRO3UC3R.js → readwise-IJQGV6IQ.js} +16 -15
  79. package/dist/{render-NEW7SROG.js → render-TMSPHRTY.js} +21 -20
  80. package/dist/{replicate-73YQLRTY.js → replicate-RJCMDB7H.js} +20 -21
  81. package/dist/{resend-R5BAZFHZ.js → resend-YYN22NIL.js} +15 -11
  82. package/dist/{rootly-5BLALPCS.js → rootly-7RXE4K7L.js} +19 -18
  83. package/dist/{scrapfly-EHFLNNUZ.js → scrapfly-ZJJ6XM4Q.js} +4 -4
  84. package/dist/{scrapingbee-UXLYS72S.js → scrapingbee-QNGEOD6D.js} +4 -4
  85. package/dist/{screenshot_fyi-KC3NLTJ6.js → screenshot_fyi-SPZAABVD.js} +4 -4
  86. package/dist/{semantic_scholar-SA5OIUWA.js → semantic_scholar-SDPVGDDI.js} +21 -20
  87. package/dist/{sentry-BAE3XLJN.js → sentry-SJ5NN4C4.js} +22 -19
  88. package/dist/{shodan-FJR6MKKW.js → shodan-YMGOY7KW.js} +4 -4
  89. package/dist/{shopify-7OA6SLN5.js → shopify-SU5ABP54.js} +23 -19
  90. package/dist/{stripe-MUWDZGGF.js → stripe-JAC425KE.js} +3 -3
  91. package/dist/{supabase-SWWGNWYR.js → supabase-GQ2OFVIL.js} +18 -17
  92. package/dist/{tavily-XQNJ5EDI.js → tavily-Y7MDA5CI.js} +20 -20
  93. package/dist/{telegram-RR6JPZOG.js → telegram-OIDT3ZLH.js} +4 -4
  94. package/dist/{telnyx-7PKXOWSO.js → telnyx-4JGMC3K7.js} +19 -19
  95. package/dist/{todoist-63X5IUJE.js → todoist-O7S2OUGX.js} +18 -17
  96. package/dist/{trello-MOG63DAI.js → trello-3LDE4TAQ.js} +4 -4
  97. package/dist/{turso-ENWTSHDL.js → turso-MUQ44KCA.js} +23 -30
  98. package/dist/{twilio-FDXUN7VX.js → twilio-VYF3HUU6.js} +3 -3
  99. package/dist/{umami-TM7PROWF.js → umami-SVQIK26J.js} +23 -28
  100. package/dist/{unsplash-NWXLKSOW.js → unsplash-333XYDCJ.js} +19 -18
  101. package/dist/{upstash_redis-CUTQ5L2N.js → upstash_redis-IUM2HDLU.js} +17 -13
  102. package/dist/{uptimerobot-NNO32M3I.js → uptimerobot-7ZRU2NJV.js} +3 -3
  103. package/dist/{v2ex-24KMSWS7.js → v2ex-AVSJCQPS.js} +35 -18
  104. package/dist/{vercel-B45LPF6I.js → vercel-J7RGXNSA.js} +21 -18
  105. package/dist/{wolfram_alpha_api-DKJWVR6G.js → wolfram_alpha_api-6CRVUZOJ.js} +4 -4
  106. package/dist/{wordpress-XYFSDZXY.js → wordpress-THOJLPKA.js} +21 -17
  107. package/dist/{workos-BEM7EPHM.js → workos-ENTGRWF2.js} +26 -27
  108. package/dist/{zhihu-CFHZ4NC7.js → zhihu-W2BXICTX.js} +21 -21
  109. package/package.json +5 -5
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createGuardedFetch
3
- } from "./chunk-3WL6EXYX.js";
3
+ } from "./chunk-ZTTN6AJG.js";
4
4
  import {
5
5
  AnnotationStore,
6
6
  CONTEXT_CAPABILITIES,
@@ -55,6 +55,7 @@ import {
55
55
  any,
56
56
  appendVia,
57
57
  array,
58
+ assertNoCollision,
58
59
  assertPluginPayloadSize,
59
60
  assertPresignTtlSec,
60
61
  assertSecretRefUse,
@@ -196,7 +197,7 @@ import {
196
197
  url,
197
198
  validatePath,
198
199
  virtualizeTools
199
- } from "./chunk-WIGQCP6O.js";
200
+ } from "./chunk-MZCIPUNQ.js";
200
201
 
201
202
  // src/config.ts
202
203
  var DEFAULT_PORT = 8787;
@@ -821,300 +822,48 @@ function createS3ObjectStore(cfg, opts) {
821
822
  };
822
823
  }
823
824
 
824
- // ../app/src/providers/toolCache.ts
825
- var KEY_PREFIX = "toolcache:";
826
- function toolCacheKey(path) {
827
- return `${KEY_PREFIX}${path}`;
828
- }
829
- function isCached(v) {
830
- return typeof v === "object" && v !== null && Array.isArray(v.tools) && typeof v.fetchedAt === "string";
831
- }
832
- function isFresh(fetchedAt, ttl, now) {
833
- const age = (Date.parse(now) - Date.parse(fetchedAt)) / 1e3;
834
- return Number.isFinite(age) && age >= 0 && age < ttl;
835
- }
836
- async function invalidateToolCache(store, path) {
837
- await store.delete(toolCacheKey(path));
838
- }
839
- function cachedTools(value) {
840
- return isCached(value) ? value.tools : null;
841
- }
842
- async function peekToolCache(store, path) {
843
- return cachedTools(await store.get(toolCacheKey(path)));
844
- }
845
- async function getTools(store, path, fetchList, opts) {
846
- if (!opts.refresh) {
847
- const raw = await store.get(toolCacheKey(path));
848
- if (isCached(raw) && isFresh(raw.fetchedAt, opts.ttl, opts.now)) {
849
- return raw.tools;
850
- }
851
- }
852
- await opts.beforeFresh?.();
853
- try {
854
- const tools = await fetchList();
855
- const record2 = { tools, fetchedAt: opts.now };
856
- await store.put(toolCacheKey(path), record2);
857
- await opts.onFresh?.(tools);
858
- return tools;
859
- } catch (error) {
860
- await opts.onFreshError?.();
861
- throw error;
862
- }
863
- }
825
+ // ../app/src/deps.ts
826
+ var TOOL_CACHE_TTL_DEFAULT = 300;
864
827
 
865
- // ../app/src/search/synchronizer.ts
866
- var DIRTY_NODE_PREFIX = "searchdirty:node:";
867
- var DIRTY_SUBTREE_PREFIX = "searchdirty:subtree:";
868
- function isMutableSearchIndex(value) {
869
- if (typeof value !== "object" || value === null) return false;
870
- const index = value;
871
- return typeof index.replace === "function" && typeof index.remove === "function" && typeof index.removePrefix === "function" && typeof index.rebuild === "function" && typeof index.initialized === "function";
872
- }
873
- function toolNode(node) {
874
- return (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config?.kind === node.kind;
875
- }
876
- function httpTools(node) {
877
- if (node.config?.kind !== "http") return null;
878
- return node.config.tools.map((tool) => {
879
- const spec = {
880
- description: tool.description,
881
- effect: effectFor(tool),
882
- name: tool.name
883
- };
884
- if (tool.inputSchema !== void 0) spec.inputSchema = tool.inputSchema;
885
- return spec;
886
- });
887
- }
888
- function deviceTools(node) {
889
- if (node.config?.kind !== "tool") return null;
890
- const cmds = node.config.providerConfig?.cmds;
891
- if (!Array.isArray(cmds)) return null;
892
- return cmds;
893
- }
894
- async function canonicalSearchTools(state, nodes) {
895
- const unique = new Map(nodes.map((node) => [node.path, node]));
896
- const dynamicPaths = [...unique.values()].filter((node) => node.kind !== "http" && deviceTools(node) === null).map((node) => node.path);
897
- const cached = dynamicPaths.length === 0 ? /* @__PURE__ */ new Map() : await state.getMany(dynamicPaths.map(toolCacheKey));
898
- const result = /* @__PURE__ */ new Map();
899
- for (const node of unique.values()) {
900
- const tools = node.kind === "http" ? httpTools(node) : deviceTools(node) ?? cachedTools(cached.get(toolCacheKey(node.path)));
901
- 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`);
902
833
  }
903
- return result;
904
- }
905
- function documentsFor(path, tools, feedback) {
906
- return tools.map((tool) => ({ path, tool, ...feedback === "" ? {} : { feedback } }));
834
+ return actual;
907
835
  }
908
- var SearchSynchronizer = class {
909
- constructor(state, search) {
910
- this.state = state;
911
- this.search = search;
912
- this.registry = new NodeRegistryStore(state);
913
- }
914
- state;
915
- search;
916
- registry;
917
- seedPromise;
918
- async mark(path, kind) {
919
- const canonical = normalizeToolSearchPath(path);
920
- const marker = {
921
- createdAt: Date.now(),
922
- // Marker 只是审计提示,正确性由每次 canonical audit 保证。每类固定一个 key,
923
- // 避免 overflow/device hello 用 UUID marker 把 KV 与扫描成本无界放大。
924
- key: `${kind === "node" ? DIRTY_NODE_PREFIX : DIRTY_SUBTREE_PREFIX}pending`,
925
- kind,
926
- path: canonical
927
- };
928
- await this.state.put(marker.key, marker);
929
- return marker;
930
- }
931
- async markNode(path) {
932
- return await this.mark(path, "node");
933
- }
934
- async markSubtree(path) {
935
- return await this.mark(path, "subtree");
936
- }
937
- async abort(marker) {
938
- if (marker !== void 0 && marker.expectedDigest === void 0) {
939
- await this.state.delete(marker.key);
940
- }
941
- }
942
- async dirtyMarkers() {
943
- const markers = [];
944
- for (const prefix of [DIRTY_NODE_PREFIX, DIRTY_SUBTREE_PREFIX]) {
945
- let cursor;
946
- do {
947
- const page = await this.state.list(prefix, {
948
- limit: 200,
949
- ...cursor === void 0 ? {} : { cursor }
950
- });
951
- for (const item of page.items) {
952
- const marker = item.value;
953
- if (typeof marker.createdAt === "number" && marker.key === item.key && (marker.kind === "node" || marker.kind === "subtree") && typeof marker.path === "string") {
954
- markers.push(marker);
955
- }
956
- }
957
- cursor = page.cursor;
958
- } while (cursor !== void 0);
959
- }
960
- return markers;
961
- }
962
- async commitMarker(marker, expectedDigest) {
963
- if (marker === void 0) return;
964
- const committed = {
965
- ...marker,
966
- committedAt: Date.now(),
967
- expectedDigest
968
- };
969
- await this.state.put(marker.key, committed);
970
- Object.assign(marker, committed);
971
- }
972
- async finishMarker(marker) {
973
- if (marker !== void 0) await this.state.delete(marker.key);
974
- }
975
- async feedbackText(path, snapshot) {
976
- if (snapshot !== void 0) return selectFeedbackSearchText(snapshot);
977
- return selectFeedbackSearchText(parseFeedbackEntries(await this.state.get(KEY_FEEDBACK + path)));
978
- }
979
- async rawTools(node, override) {
980
- if (override !== void 0) return [...override];
981
- if (node.kind === "http") return httpTools(node);
982
- const device = deviceTools(node);
983
- if (device !== null) return device;
984
- if (node.kind === "mcp" || node.kind === "tool") {
985
- return await peekToolCache(this.state, node.path);
986
- }
987
- return null;
988
- }
989
- indexableRecords(path, tools, feedback) {
990
- try {
991
- return serializeToolSearchSnapshot(path, tools, feedback);
992
- } catch (error) {
993
- if (isTBError(error)) return null;
994
- throw error;
995
- }
996
- }
997
- async reconcileNode(path, opts = {}) {
998
- const canonical = normalizeToolSearchPath(path);
999
- const node = await this.registry.get(canonical).catch(() => null);
1000
- if (node === null || !toolNode(node)) {
1001
- await this.commitMarker(opts.marker, null);
1002
- await this.search.remove(canonical);
1003
- await this.finishMarker(opts.marker);
1004
- return;
1005
- }
1006
- const tools = await this.rawTools(node, opts.tools);
1007
- if (tools === null) {
1008
- await this.search.remove(canonical);
1009
- await this.finishMarker(opts.marker);
1010
- return;
1011
- }
1012
- if (tools.length === 0) {
1013
- await this.commitMarker(opts.marker, null);
1014
- await this.search.remove(canonical);
1015
- await this.finishMarker(opts.marker);
1016
- return;
1017
- }
1018
- const root = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
1019
- if (root.truncated) {
1020
- await this.finishMarker(opts.marker);
1021
- return;
1022
- }
1023
- const feedback = await this.feedbackText(canonical, opts.feedback);
1024
- const records = this.indexableRecords(canonical, tools, feedback);
1025
- if (records === null) {
1026
- await this.commitMarker(opts.marker, null);
1027
- await this.search.remove(canonical);
1028
- await this.finishMarker(opts.marker);
1029
- return;
1030
- }
1031
- const digest = records.length === 0 ? null : toolSearchSnapshotDigest(records);
1032
- await this.commitMarker(opts.marker, digest);
1033
- await this.search.replace(canonical, tools, {
1034
- feedback
1035
- });
1036
- await this.finishMarker(opts.marker);
1037
- }
1038
- /** mutation 热路径使用:canonical 已成功时,派生同步失败只保留 marker。 */
1039
- async reconcileNodeQuietly(path, opts = {}) {
1040
- try {
1041
- await this.reconcileNode(path, opts);
1042
- } catch {
1043
- }
1044
- }
1045
- async rebuildAll(markers = [], opts = {}) {
1046
- const snapshot = opts.canonicalNodes === void 0 ? await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT) : { items: [...opts.canonicalNodes], truncated: false };
1047
- if (snapshot.truncated) return false;
1048
- const nodes = snapshot.items.filter(toolNode);
1049
- const authoritativeEmpty = new Set(opts.authoritativeEmpty ?? []);
1050
- const keys = nodes.flatMap((node) => [KEY_FEEDBACK + node.path, toolCacheKey(node.path)]);
1051
- const values = await this.state.getMany(keys);
1052
- const documents = [];
1053
- for (const node of nodes) {
1054
- if (authoritativeEmpty.has(node.path)) continue;
1055
- let tools;
1056
- if (node.kind === "http") {
1057
- tools = httpTools(node);
1058
- } else {
1059
- tools = deviceTools(node) ?? cachedTools(values.get(toolCacheKey(node.path)));
1060
- }
1061
- if (tools === null) continue;
1062
- const feedback = selectFeedbackSearchText(
1063
- parseFeedbackEntries(values.get(KEY_FEEDBACK + node.path))
1064
- );
1065
- if (this.indexableRecords(node.path, tools, feedback) === null) continue;
1066
- documents.push(...documentsFor(node.path, tools, feedback));
1067
- }
1068
- await this.search.rebuild(documents);
1069
- await Promise.all(markers.map(async (marker) => {
1070
- await this.state.delete(marker.key).catch(() => {
1071
- });
1072
- }));
1073
- return true;
1074
- }
1075
- /** 每次搜索前审计 canonical 快照;同快照 rebuild 不 bump revision。 */
1076
- async ensureReady() {
1077
- const snapshot = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
1078
- if (snapshot.truncated) {
1079
- if (await this.search.initialized()) return;
1080
- throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
1081
- }
1082
- const dirty = await this.dirtyMarkers();
1083
- await this.rebuildAll(dirty, { canonicalNodes: snapshot.items });
1084
- }
1085
- /** 首次 registry/device mutation 前建立 last-known-good,避免并发溢出抢在首次 seed 前。 */
1086
- async ensureSeeded() {
1087
- if (await this.search.initialized()) return;
1088
- if (this.seedPromise === void 0) {
1089
- this.seedPromise = (async () => {
1090
- if (!await this.rebuildAll()) {
1091
- throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
1092
- }
1093
- })();
1094
- }
1095
- try {
1096
- await this.seedPromise;
1097
- } finally {
1098
- this.seedPromise = void 0;
1099
- }
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");
1100
840
  }
1101
- async removeSubtreeQuietly(path, marker) {
1102
- try {
1103
- const canonical = normalizeToolSearchPath(path);
1104
- if ((await this.registry.subtree(canonical)).length === 0) {
1105
- await this.commitMarker(marker, null);
1106
- await this.search.removePrefix(canonical);
1107
- await this.finishMarker(marker);
1108
- } else {
1109
- await this.rebuildAll(marker === void 0 ? [] : [marker]);
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);
1110
849
  }
1111
- } catch {
850
+ return void 0;
1112
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;
1113
864
  }
1114
- };
1115
-
1116
- // ../app/src/deps.ts
1117
- var TOOL_CACHE_TTL_DEFAULT = 300;
865
+ return cursor;
866
+ }
1118
867
 
1119
868
  // ../app/src/storeRefToken.ts
1120
869
  async function hmacKey(secret) {
@@ -1173,13 +922,6 @@ var STORE_READ_TTL_SEC_DEFAULT = 15 * 60;
1173
922
  var STORE_CALL_MAX_OBJECTS_DEFAULT = 4;
1174
923
  var STORE_CALL_MAX_OBJECT_BYTES_DEFAULT = 256 * 1024 * 1024;
1175
924
  var STORE_CALL_MAX_BYTES_DEFAULT = 512 * 1024 * 1024;
1176
- function positiveInt(value, fallback, field) {
1177
- const actual = value ?? fallback;
1178
- if (!Number.isSafeInteger(actual) || actual < 1) {
1179
- throw new TBError("invalid_argument", `${field} must be a positive integer`);
1180
- }
1181
- return actual;
1182
- }
1183
925
  function randomSecret() {
1184
926
  const bytes = crypto.getRandomValues(new Uint8Array(32));
1185
927
  let out = "";
@@ -1512,44 +1254,28 @@ async function cleanupDefaultStore(deps, opts = {}) {
1512
1254
  const store = await defaultStoreRuntime(deps);
1513
1255
  const maxPages = Math.min(positiveInt(opts.maxPages, 8, "maxPages"), 64);
1514
1256
  const limit = opts.limit === void 0 ? void 0 : positiveInt(opts.limit, 200, "limit");
1515
- const rawProgress = await deps.state.get(KEY_STORE_CLEANUP_PROGRESS);
1516
- let progress = rawProgress === null ? null : cleanupProgress(rawProgress);
1517
- let cursors = progress?.cursors;
1518
1257
  const aggregate = emptyCleanupResult();
1519
- for (let pageNumber = 0; pageNumber < maxPages; pageNumber++) {
1520
- const page = await store.service.cleanup({
1521
- ...limit === void 0 ? {} : { limit },
1522
- ...cursors === void 0 ? {} : { cursors },
1523
- // staging 不使用权威状态 cursor;同一 host tick 只运行一次,避免
1524
- // maxPages 内重复扫描同一 driver maintenance 范围。
1525
- runDriverMaintenance: pageNumber === 0
1526
- });
1527
- addCleanupResult(aggregate, page);
1528
- if (page.cursors === void 0) {
1529
- if (progress !== null) {
1530
- await deps.state.compareAndSwap(
1531
- KEY_STORE_CLEANUP_PROGRESS,
1532
- progress.revision,
1533
- null
1534
- );
1535
- }
1536
- return aggregate;
1537
- }
1538
- const next = {
1539
- cursors: page.cursors,
1540
- revision: (progress?.revision ?? 0) + 1
1541
- };
1542
- const advanced = await deps.state.compareAndSwap(
1543
- KEY_STORE_CLEANUP_PROGRESS,
1544
- progress?.revision ?? null,
1545
- next
1546
- );
1547
- if (!advanced) {
1548
- return aggregate;
1549
- }
1550
- progress = next;
1551
- cursors = next.cursors;
1552
- }
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
+ });
1553
1279
  return { ...aggregate, ...cursors === void 0 ? {} : { cursors } };
1554
1280
  }
1555
1281
 
@@ -19809,8 +19535,6 @@ function createPluginToolProvider(opts) {
19809
19535
 
19810
19536
  // ../app/src/providerOAuth.ts
19811
19537
  var KEY_TOKENS = "puoauth:token:";
19812
- var STATE_TTL_SEC2 = 600;
19813
- var PROVIDER_OAUTH_CALLBACK_PATH = "/~oauth/callback";
19814
19538
  async function invalidateProviderOAuth(store, nodePath) {
19815
19539
  await store.delete(KEY_TOKENS + nodePath);
19816
19540
  }
@@ -19885,15 +19609,10 @@ async function requestTokens(opts) {
19885
19609
  throw err;
19886
19610
  }
19887
19611
  }
19888
- function base64url(bytes) {
19889
- let binary = "";
19890
- for (const byte of bytes) binary += String.fromCharCode(byte);
19891
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
19892
- }
19893
19612
  async function createPkce() {
19894
- const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
19613
+ const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
19895
19614
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
19896
- return { verifier, challenge: base64url(new Uint8Array(digest)) };
19615
+ return { verifier, challenge: base64urlEncode(new Uint8Array(digest)) };
19897
19616
  }
19898
19617
  function requireProviderOAuthFetch(fetcher) {
19899
19618
  if (fetcher === void 0) {
@@ -19929,7 +19648,7 @@ async function startProviderAuthorization(opts) {
19929
19648
  {
19930
19649
  p: opts.nodePath,
19931
19650
  v: pkce?.verifier ?? "",
19932
- exp: Math.floor(opts.now.getTime() / 1e3) + STATE_TTL_SEC2
19651
+ exp: Math.floor(opts.now.getTime() / 1e3) + STATE_TTL_SEC
19933
19652
  },
19934
19653
  opts.encryptionKey
19935
19654
  );
@@ -19938,7 +19657,7 @@ async function startProviderAuthorization(opts) {
19938
19657
  authorizationUrl: buildAuthorizationUrl({
19939
19658
  config: opts.config,
19940
19659
  clientId: client.clientId,
19941
- redirectUri: opts.origin + PROVIDER_OAUTH_CALLBACK_PATH,
19660
+ redirectUri: opts.origin + OAUTH_CALLBACK_PATH,
19942
19661
  state,
19943
19662
  ...pkce === void 0 ? {} : { codeChallenge: pkce.challenge }
19944
19663
  })
@@ -19951,7 +19670,7 @@ async function finishProviderAuthorization(opts) {
19951
19670
  config: opts.config,
19952
19671
  fetcher,
19953
19672
  now: opts.now,
19954
- grant: { code: opts.code, redirectUri: opts.origin + PROVIDER_OAUTH_CALLBACK_PATH },
19673
+ grant: { code: opts.code, redirectUri: opts.origin + OAUTH_CALLBACK_PATH },
19955
19674
  ...opts.codeVerifier === "" ? {} : { codeVerifier: opts.codeVerifier },
19956
19675
  ...client
19957
19676
  });
@@ -19981,21 +19700,61 @@ async function resolveProviderAccessToken(opts) {
19981
19700
  return refreshed.accessToken;
19982
19701
  }
19983
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
+
19984
19744
  // ../app/src/toolNodes.ts
19985
- function upstreamTools(node, provider, deps, refresh, now) {
19745
+ function upstreamTools(node, provider, deps, refresh, now, searchSync) {
19986
19746
  if (node.kind === "mcp" || node.kind === "tool") {
19987
- const sync = isMutableSearchIndex(deps.search) ? new SearchSynchronizer(deps.state, deps.search) : void 0;
19988
19747
  let marker;
19989
19748
  return getTools(deps.state, node.path, () => provider.list(), {
19990
19749
  refresh,
19991
19750
  ttl: deps.toolCacheTtlSec ?? TOOL_CACHE_TTL_DEFAULT,
19992
19751
  now,
19993
- ...sync === void 0 ? {} : {
19752
+ ...searchSync === void 0 ? {} : {
19994
19753
  beforeFresh: async () => {
19995
- marker = await sync.markNode(node.path);
19754
+ marker = await searchSync.markNode(node.path);
19996
19755
  },
19997
- onFreshError: async () => await sync.abort(marker),
19998
- onFresh: async (tools) => await sync.reconcileNodeQuietly(node.path, { marker, tools })
19756
+ onFreshError: async () => await searchSync.abort(marker),
19757
+ onFresh: async (tools) => await searchSync.reconcileNodeQuietly(node.path, { marker, tools })
19999
19758
  }
20000
19759
  });
20001
19760
  }
@@ -20130,11 +19889,11 @@ async function providerFor(node, ctx, deps) {
20130
19889
  }
20131
19890
  throw TBError.unimplemented(`kind '${node.kind}' has no tool provider`);
20132
19891
  }
20133
- async function refreshDynamicSearchNode(node, ctx, deps) {
19892
+ async function refreshDynamicSearchNode(node, ctx, deps, searchSync) {
20134
19893
  if (node.kind !== "mcp" && node.kind !== "tool" || deviceToolMarker(node) !== null) return false;
20135
19894
  try {
20136
19895
  const provider = await providerFor(node, ctx, deps);
20137
- await upstreamTools(node, provider, deps, true, (/* @__PURE__ */ new Date()).toISOString());
19896
+ await upstreamTools(node, provider, deps, true, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
20138
19897
  return true;
20139
19898
  } catch {
20140
19899
  return false;
@@ -20149,7 +19908,7 @@ async function refreshDynamicToolCache(node, ctx, deps) {
20149
19908
  now: (/* @__PURE__ */ new Date()).toISOString()
20150
19909
  });
20151
19910
  }
20152
- async function toolHelpModelFor(c, ctx, registry, path, deps) {
19911
+ async function toolHelpModelFor(c, ctx, registry, path, deps, searchSync) {
20153
19912
  let resolved;
20154
19913
  try {
20155
19914
  resolved = await registry.resolve(path);
@@ -20170,7 +19929,7 @@ async function toolHelpModelFor(c, ctx, registry, path, deps) {
20170
19929
  }
20171
19930
  const provider = await providerFor(node, ctx, deps);
20172
19931
  const refresh = c.req.query("refresh") === "1";
20173
- 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);
20174
19933
  const { exposed } = virtualizeTools(node.virtualize, raw);
20175
19934
  const tool = exposed.find((t) => t.name === rest);
20176
19935
  if (tool === void 0) return null;
@@ -20323,9 +20082,6 @@ async function verifyRefToken(token, secret) {
20323
20082
  function localContext(deps, node) {
20324
20083
  return deps.locals?.context?.(node.path) ?? null;
20325
20084
  }
20326
- function localCapabilities(provider) {
20327
- return contextCapabilitiesOf(provider);
20328
- }
20329
20085
  function parseS3Credentials(raw, refName) {
20330
20086
  try {
20331
20087
  const v = JSON.parse(raw);
@@ -20402,13 +20158,8 @@ async function createContextUploadGrant(node, cfg, deps, input) {
20402
20158
  uploadGrantTtlSec
20403
20159
  }, input);
20404
20160
  }
20405
- async function contextProviderFor(node, cfg, deps, requestUrl) {
20406
- const objects = await contextObjectStoreFor(cfg, deps);
20407
- const opts = {
20408
- nsPath: node.path,
20409
- keyPrefix: contextKeyPrefix(cfg, node.path),
20410
- readOnly: cfg.readOnly ?? false
20411
- };
20161
+ function objectProviderOpts(node, keyPrefix, readOnly, deps, requestUrl) {
20162
+ const opts = { nsPath: node.path, keyPrefix, readOnly };
20412
20163
  if (deps.refThresholdBytes !== void 0) opts.refThresholdBytes = deps.refThresholdBytes;
20413
20164
  if (deps.refTtlSec !== void 0) opts.presignTtlSec = deps.refTtlSec;
20414
20165
  const encKey = deps.encryptionKey;
@@ -20420,7 +20171,17 @@ async function contextProviderFor(node, cfg, deps, requestUrl) {
20420
20171
  return `${origin}/~ref/${await signRefToken({ p: node.path, k: key, exp }, encKey)}`;
20421
20172
  };
20422
20173
  }
20423
- return createObjectContextProvider(objects, opts);
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
+ ));
20424
20185
  }
20425
20186
  function skillhubKeyPrefix(cfg, nodePath) {
20426
20187
  const prefix = cfg.providerConfig?.prefix;
@@ -20429,29 +20190,13 @@ function skillhubKeyPrefix(cfg, nodePath) {
20429
20190
  }
20430
20191
  async function skillhubProviderFor(node, cfg, deps, requestUrl) {
20431
20192
  const objects = await contextObjectStoreFor(cfg, deps);
20432
- const opts = {
20433
- nsPath: node.path,
20434
- keyPrefix: skillhubKeyPrefix(cfg, node.path),
20435
- readOnly: cfg.readOnly ?? false
20436
- };
20437
- if (deps.refThresholdBytes !== void 0) opts.refThresholdBytes = deps.refThresholdBytes;
20438
- if (deps.refTtlSec !== void 0) opts.presignTtlSec = deps.refTtlSec;
20439
- const encKey = deps.encryptionKey;
20440
- if (encKey !== void 0) {
20441
- const origin = new URL(requestUrl).origin;
20442
- const relayTtlSec = deps.refTtlSec ?? PRESIGN_TTL_SEC_DEFAULT;
20443
- opts.relayRefUrl = async (key) => {
20444
- const exp = Math.floor(Date.now() / 1e3) + relayTtlSec;
20445
- return `${origin}/~ref/${await signRefToken({ p: node.path, k: key, exp }, encKey)}`;
20446
- };
20447
- }
20448
- return createSkillhubProvider(objects, opts);
20449
- }
20450
- async function dispatchSkillhubCmd2(provider, command, args) {
20451
- return await dispatchSkillhubCmd(provider, command, args);
20452
- }
20453
- async function dispatchContextCmd2(provider, command, args) {
20454
- 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
+ ));
20455
20200
  }
20456
20201
  async function assertContextAlive(node, cfg, registry) {
20457
20202
  if (!isContextExpired(node.createdAt, cfg.ttl, Date.now())) return;
@@ -20522,6 +20267,257 @@ async function assertSkillhubConfig(config, deps) {
20522
20267
  }
20523
20268
  }
20524
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
+
20525
20521
  // ../app/src/deviceHello.ts
20526
20522
  function assertDeviceId(deviceId) {
20527
20523
  if (!/^[A-Za-z0-9._-]+$/.test(deviceId)) {
@@ -20561,6 +20557,7 @@ function customNodeInput(mountPath, deviceId, raw) {
20561
20557
  return input;
20562
20558
  }
20563
20559
  function nodesForHello(mountPath, deviceId, expose) {
20560
+ const rawTargets = [mountPath];
20564
20561
  const nodes = [
20565
20562
  {
20566
20563
  path: mountPath,
@@ -20569,6 +20566,7 @@ function nodesForHello(mountPath, deviceId, expose) {
20569
20566
  }
20570
20567
  ];
20571
20568
  if (expose.shell !== void 0) {
20569
+ rawTargets.push(joinTreePath(mountPath, "shell"));
20572
20570
  nodes.push({
20573
20571
  path: joinTreePath(mountPath, "shell"),
20574
20572
  kind: "device",
@@ -20577,6 +20575,7 @@ function nodesForHello(mountPath, deviceId, expose) {
20577
20575
  });
20578
20576
  }
20579
20577
  if (expose.fs !== void 0) {
20578
+ rawTargets.push(joinTreePath(mountPath, "fs"));
20580
20579
  nodes.push({
20581
20580
  path: joinTreePath(mountPath, "fs"),
20582
20581
  kind: "context",
@@ -20590,11 +20589,13 @@ function nodesForHello(mountPath, deviceId, expose) {
20590
20589
  });
20591
20590
  }
20592
20591
  for (const raw of expose.nodes ?? []) {
20592
+ rawTargets.push(joinTreePath(mountPath, raw.path));
20593
20593
  nodes.push(customNodeInput(mountPath, deviceId, raw));
20594
20594
  }
20595
+ assertNoCollision(rawTargets, `\u8BBE\u5907 '${deviceId}' \u7684 expose \u8DEF\u5F84`);
20595
20596
  return nodes;
20596
20597
  }
20597
- async function assertRegisterPath(registry, ctx, targetPath) {
20598
+ async function assertRegisterPath(registry, ctx, targetPath, reservedRoots) {
20598
20599
  if (!check(ctx, targetPath, "register").allow) {
20599
20600
  throw new TBError("permission_denied", `no scope grants 'register' on '${targetPath}'`);
20600
20601
  }
@@ -20612,7 +20613,8 @@ async function assertRegisterPath(registry, ctx, targetPath) {
20612
20613
  },
20613
20614
  targetPath,
20614
20615
  action: "write",
20615
- existing
20616
+ existing,
20617
+ ...reservedRoots !== void 0 ? { reservedRoots } : {}
20616
20618
  });
20617
20619
  if (!res.allow) throw res.error;
20618
20620
  }
@@ -20633,7 +20635,7 @@ async function processDeviceHello(opts) {
20633
20635
  const inputs = nodesForHello(mountPath, hello.deviceId, hello.expose);
20634
20636
  const registry = new NodeRegistryStore(store);
20635
20637
  for (const input of inputs) {
20636
- await assertRegisterPath(registry, authCtx, input.path);
20638
+ await assertRegisterPath(registry, authCtx, input.path, opts.reservedRoots);
20637
20639
  assertSecretRefUse(authCtx.scopes, input.config);
20638
20640
  }
20639
20641
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
@@ -20662,15 +20664,43 @@ async function processDeviceHello(opts) {
20662
20664
  return { mountPath, keyId: authCtx.keyId, searchIndexed };
20663
20665
  }
20664
20666
 
20665
- // ../app/src/deviceMailbox.ts
20666
- var KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS = "sys:device-mailbox-cleanup-progress:v1";
20667
- function positiveInt2(value, fallback, field) {
20668
- const actual = value ?? fallback;
20669
- if (!Number.isSafeInteger(actual) || actual < 1) {
20670
- throw new TBError("invalid_argument", `${field} must be a positive integer`);
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 {
20671
20687
  }
20672
- return actual;
20673
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";
20674
20704
  function parseProgress(value) {
20675
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");
20676
20706
  return value;
@@ -20684,39 +20714,27 @@ async function cleanupDeviceMailbox(deps, opts = {}) {
20684
20714
  throw new TBError("unavailable", "device mailbox cleanup requires StateStore.compareAndSwap");
20685
20715
  }
20686
20716
  const mailbox = createDeviceMailboxService(deps);
20687
- const maxPages = Math.min(positiveInt2(opts.maxPages, 8, "maxPages"), 64);
20688
- const limit = Math.min(positiveInt2(opts.limit, 200, "limit"), 200);
20689
- const raw = await deps.state.get(KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS);
20690
- let progress = raw === null ? null : parseProgress(raw);
20691
- let cursor = progress?.cursor;
20717
+ const maxPages = Math.min(positiveInt(opts.maxPages, 8, "maxPages"), 64);
20718
+ const limit = Math.min(positiveInt(opts.limit, 200, "limit"), 200);
20692
20719
  const aggregate = { deleted: 0, expired: 0, scanned: 0 };
20693
- for (let pageNumber = 0; pageNumber < maxPages; pageNumber++) {
20694
- const page = await mailbox.cleanup({ limit, ...cursor === void 0 ? {} : { cursor } });
20695
- aggregate.deleted += page.deleted;
20696
- aggregate.expired += page.expired;
20697
- aggregate.scanned += page.scanned;
20698
- if (page.cursor === void 0) {
20699
- if (progress !== null) {
20700
- await deps.state.compareAndSwap(
20701
- KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
20702
- progress.revision,
20703
- null
20704
- );
20705
- }
20706
- return aggregate;
20707
- }
20708
- const next = {
20709
- cursor: page.cursor,
20710
- revision: (progress?.revision ?? 0) + 1
20711
- };
20712
- if (!await deps.state.compareAndSwap(
20713
- KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
20714
- progress?.revision ?? null,
20715
- next
20716
- )) return aggregate;
20717
- progress = next;
20718
- cursor = next.cursor;
20719
- }
20720
+ 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
+ });
20720
20738
  return { ...aggregate, ...cursor === void 0 ? {} : { cursor } };
20721
20739
  }
20722
20740
 
@@ -37320,6 +37338,99 @@ async function handleFeedbackDelete(c, env) {
37320
37338
  return feedbackJson(feedbackRemoveResponseSchema, { ok: true });
37321
37339
  }
37322
37340
 
37341
+ // ../app/src/responses.ts
37342
+ function tbErrorResponse(err) {
37343
+ return new Response(JSON.stringify(tbErrorBodySchema.parse(err.toJSON())), {
37344
+ status: err.httpStatus,
37345
+ headers: { "content-type": "application/json; charset=utf-8" }
37346
+ });
37347
+ }
37348
+ function withSecurityHeaders(res) {
37349
+ if (res.status === 101 || res.webSocket != null) return res;
37350
+ const apply = (headers) => {
37351
+ if (!headers.has("content-security-policy")) {
37352
+ headers.set(
37353
+ "content-security-policy",
37354
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https: http:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'"
37355
+ );
37356
+ }
37357
+ headers.set("x-content-type-options", "nosniff");
37358
+ headers.set("x-frame-options", "DENY");
37359
+ headers.set("referrer-policy", "no-referrer");
37360
+ };
37361
+ try {
37362
+ apply(res.headers);
37363
+ return res;
37364
+ } catch {
37365
+ const headers = new Headers(res.headers);
37366
+ apply(headers);
37367
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
37368
+ }
37369
+ }
37370
+ async function runHandler(fn) {
37371
+ try {
37372
+ return await fn();
37373
+ } catch (err) {
37374
+ if (isTBError(err)) return tbErrorResponse(err);
37375
+ return tbErrorResponse(new TBError("internal", "internal error"));
37376
+ }
37377
+ }
37378
+ function renderHelp(model, rep) {
37379
+ if (rep === "json") {
37380
+ return new Response(JSON.stringify(helpJsonSchema.parse(renderHelpJson(model))), {
37381
+ headers: { "content-type": contentTypeFor("json") }
37382
+ });
37383
+ }
37384
+ if (rep === "markdown") {
37385
+ return new Response(renderHelpMarkdown(model), {
37386
+ headers: { "content-type": contentTypeFor("markdown") }
37387
+ });
37388
+ }
37389
+ return new Response(renderHelpDsl(model), {
37390
+ headers: { "content-type": contentTypeFor("dsl") }
37391
+ });
37392
+ }
37393
+ async function enrichHelp(model, path, store) {
37394
+ try {
37395
+ const [annotation, feedback] = await Promise.all([
37396
+ new AnnotationStore(store).get(path),
37397
+ path === "" ? Promise.resolve([]) : new FeedbackStore(store).helpItems(path)
37398
+ ]);
37399
+ return {
37400
+ ...model,
37401
+ ...annotation !== null ? { note: annotation.text } : {},
37402
+ ...feedback.length > 0 ? { feedback } : {}
37403
+ };
37404
+ } catch {
37405
+ return model;
37406
+ }
37407
+ }
37408
+ function renderResult(value, rep) {
37409
+ const json = JSON.stringify(value ?? null);
37410
+ if (rep === "json") {
37411
+ return new Response(json, { headers: { "content-type": contentTypeFor("json") } });
37412
+ }
37413
+ return new Response(`\`\`\`json
37414
+ ${json}
37415
+ \`\`\`
37416
+ `, {
37417
+ headers: { "content-type": contentTypeFor("markdown") }
37418
+ });
37419
+ }
37420
+ function renderTreeDsl(tree) {
37421
+ const lines = [];
37422
+ const walk = (n, depth) => {
37423
+ const indent = " ".repeat(depth);
37424
+ const label = n.path === "" ? "/" : n.path;
37425
+ const trunc = n.truncated ? " \u2026" : "";
37426
+ lines.push(`${indent}${label} [${n.kind}] ${n.description}${trunc}`);
37427
+ for (const child of n.children ?? []) walk(child, depth + 1);
37428
+ };
37429
+ walk(tree, 0);
37430
+ return `${lines.join("\n")}
37431
+ `;
37432
+ }
37433
+
37323
37434
  // ../app/src/routes/deviceMailbox.ts
37324
37435
  var DEVICE_MAILBOX_IDEMPOTENCY_HEADER = "x-tb-idempotency-key";
37325
37436
  var DEVICE_MAILBOX_HTTP_BODY_BYTES = 272 * 1024;
@@ -37395,7 +37506,7 @@ function canManage(ctx, operation) {
37395
37506
  function assertCanManage(ctx, operation) {
37396
37507
  if (!canManage(ctx, operation)) throw TBError.notFound("device operation not found");
37397
37508
  }
37398
- async function assertCurrentDeviceCredential(c, target) {
37509
+ async function assertCurrentDeviceCredential(c, target, reservedRoots) {
37399
37510
  const ctx = c.get("ctx");
37400
37511
  if (ctx.keyId !== target.deviceKeyId) {
37401
37512
  throw new TBError("permission_denied", "device credential does not own this operation");
@@ -37414,7 +37525,8 @@ async function assertCurrentDeviceCredential(c, target) {
37414
37525
  scopes: ctx.scopes,
37415
37526
  ...ctx.registerPaths === void 0 ? {} : { registerPaths: ctx.registerPaths }
37416
37527
  },
37417
- targetPath: target.mountPath
37528
+ targetPath: target.mountPath,
37529
+ ...reservedRoots === void 0 ? {} : { reservedRoots }
37418
37530
  });
37419
37531
  if (!allowed.allow) throw allowed.error;
37420
37532
  }
@@ -37438,212 +37550,118 @@ async function enqueueDeviceCommand(c, env, input) {
37438
37550
  response.headers.set("x-tb-delivery", "mailbox");
37439
37551
  return response;
37440
37552
  }
37441
- async function handleDeviceMailboxControl(c, env) {
37442
- const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
37443
- const body = await mailboxJsonObject(c);
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));
37444
37561
  const mailbox = env.mailbox();
37445
37562
  const ctx = c.get("ctx");
37446
- if (path === "/~device/operations/get") {
37447
- const input = parseBody(deviceOperationIdentityRequestSchema, body);
37448
- const result = await mailbox.get(input.deviceId, input.operationId);
37449
- assertCanManage(ctx, result);
37450
- return c.json(deviceOperationDetailSchema.parse(result));
37451
- }
37452
- if (path === "/~device/operations/list") {
37453
- const input = parseBody(deviceOperationListRequestSchema, body);
37454
- const limit = Math.min(
37455
- input.opts?.limit ?? DEVICE_MAILBOX_LIST_LIMIT_DEFAULT,
37456
- DEVICE_MAILBOX_LIST_LIMIT_MAX
37457
- );
37458
- let cursor = input.opts?.cursor;
37459
- const items = [];
37460
- while (items.length < limit) {
37461
- const previous = cursor;
37462
- const page = await mailbox.list({
37463
- deviceId: input.deviceId,
37464
- limit: limit - items.length,
37465
- ...cursor === void 0 ? {} : { cursor },
37466
- ...input.opts?.states === void 0 ? {} : { states: input.opts.states }
37467
- });
37468
- items.push(...page.items.filter((item) => canManage(ctx, item)));
37469
- cursor = page.cursor;
37470
- if (cursor === void 0) {
37471
- return c.json(deviceOperationListResponseSchema.parse({ items }));
37472
- }
37473
- if (cursor === previous) {
37474
- throw new TBError("internal", "device mailbox list cursor did not advance");
37475
- }
37476
- }
37477
- const resumeCursor = cursor;
37478
- if (resumeCursor === void 0) {
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) {
37479
37580
  return c.json(deviceOperationListResponseSchema.parse({ items }));
37480
37581
  }
37481
- let probeCursor = resumeCursor;
37482
- while (probeCursor !== void 0) {
37483
- const previous = probeCursor;
37484
- const page = await mailbox.list({
37485
- deviceId: input.deviceId,
37486
- cursor: probeCursor,
37487
- limit: DEVICE_MAILBOX_LIST_LIMIT_MAX,
37488
- ...input.opts?.states === void 0 ? {} : { states: input.opts.states }
37489
- });
37490
- if (page.items.some((item) => canManage(ctx, item))) {
37491
- return c.json(deviceOperationListResponseSchema.parse({ items, cursor: resumeCursor }));
37492
- }
37493
- probeCursor = page.cursor;
37494
- if (probeCursor === previous) {
37495
- throw new TBError("internal", "device mailbox list cursor did not advance");
37496
- }
37582
+ if (cursor === previous) {
37583
+ throw new TBError("internal", "device mailbox list cursor did not advance");
37497
37584
  }
37498
- return c.json(deviceOperationListResponseSchema.parse({ items }));
37499
- }
37500
- if (path === "/~device/operations/cancel") {
37501
- const input = parseBody(deviceOperationIdentityRequestSchema, body);
37502
- const current = await mailbox.get(input.deviceId, input.operationId);
37503
- assertCanManage(ctx, current);
37504
- return c.json(deviceOperationDetailSchema.parse(
37505
- await mailbox.cancel(input.deviceId, input.operationId)
37506
- ));
37507
37585
  }
37508
- throw TBError.notFound("no such path");
37509
- }
37510
- async function handleDeviceMailboxData(c, env) {
37511
- const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
37512
- const body = await mailboxJsonObject(c);
37513
- const mailbox = env.mailbox();
37514
- const ctx = c.get("ctx");
37515
- const authorize = async (target) => await assertCurrentDeviceCredential(c, target);
37516
- if (path === "/~device/mailbox/claim") {
37517
- const input = parseBody(deviceOperationClaimRequestSchema, body);
37518
- return c.json(deviceOperationClaimResponseSchema.parse(await mailbox.claim({
37519
- ...input,
37520
- deviceKeyId: ctx.keyId,
37521
- authorize
37522
- })));
37586
+ const resumeCursor = cursor;
37587
+ if (resumeCursor === void 0) {
37588
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37523
37589
  }
37524
- if (path === "/~device/mailbox/renew") {
37525
- const input = parseBody(deviceOperationRenewRequestSchema, body);
37526
- return c.json(deviceOperationRenewResponseSchema.parse(await mailbox.renew({
37527
- ...input,
37528
- deviceKeyId: ctx.keyId,
37529
- authorize
37530
- })));
37531
- }
37532
- if (path === "/~device/mailbox/complete") {
37533
- const input = parseBody(deviceOperationCompleteRequestSchema, body);
37534
- let completion;
37535
- if (input.outcome === "succeeded") {
37536
- completion = { outcome: "succeeded", result: input.result };
37537
- } else if (input.outcome === "result_unknown") {
37538
- completion = {
37539
- outcome: "result_unknown",
37540
- ...input.error === void 0 ? {} : { error: input.error }
37541
- };
37542
- } else {
37543
- completion = { outcome: input.outcome, error: input.error };
37544
- }
37545
- return c.json(deviceOperationDetailSchema.parse(await mailbox.complete({
37590
+ let probeCursor = resumeCursor;
37591
+ while (probeCursor !== void 0) {
37592
+ const previous = probeCursor;
37593
+ const page = await mailbox.list({
37546
37594
  deviceId: input.deviceId,
37547
- operationId: input.operationId,
37548
- leaseId: input.leaseId,
37549
- deviceKeyId: ctx.keyId,
37550
- authorize
37551
- }, completion)));
37552
- }
37553
- throw TBError.notFound("no such path");
37554
- }
37555
-
37556
- // ../app/src/responses.ts
37557
- function tbErrorResponse(err) {
37558
- return new Response(JSON.stringify(tbErrorBodySchema.parse(err.toJSON())), {
37559
- status: err.httpStatus,
37560
- headers: { "content-type": "application/json; charset=utf-8" }
37561
- });
37562
- }
37563
- function withSecurityHeaders(res) {
37564
- if (res.status === 101 || res.webSocket != null) return res;
37565
- const apply = (headers) => {
37566
- if (!headers.has("content-security-policy")) {
37567
- headers.set(
37568
- "content-security-policy",
37569
- "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https: http:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'"
37570
- );
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");
37571
37605
  }
37572
- headers.set("x-content-type-options", "nosniff");
37573
- headers.set("x-frame-options", "DENY");
37574
- headers.set("referrer-policy", "no-referrer");
37575
- };
37576
- try {
37577
- apply(res.headers);
37578
- return res;
37579
- } catch {
37580
- const headers = new Headers(res.headers);
37581
- apply(headers);
37582
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
37583
37606
  }
37607
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37584
37608
  }
37585
- async function runHandler(fn) {
37586
- try {
37587
- return await fn();
37588
- } catch (err) {
37589
- if (isTBError(err)) return tbErrorResponse(err);
37590
- return tbErrorResponse(new TBError("internal", "internal error"));
37591
- }
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
+ })));
37592
37628
  }
37593
- function renderHelp(model, rep) {
37594
- if (rep === "json") {
37595
- return new Response(JSON.stringify(helpJsonSchema.parse(renderHelpJson(model))), {
37596
- headers: { "content-type": contentTypeFor("json") }
37597
- });
37598
- }
37599
- if (rep === "markdown") {
37600
- return new Response(renderHelpMarkdown(model), {
37601
- headers: { "content-type": contentTypeFor("markdown") }
37602
- });
37603
- }
37604
- return new Response(renderHelpDsl(model), {
37605
- headers: { "content-type": contentTypeFor("dsl") }
37606
- });
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
+ })));
37607
37636
  }
37608
- async function enrichHelp(model, path, store) {
37609
- try {
37610
- const [annotation, feedback] = await Promise.all([
37611
- new AnnotationStore(store).get(path),
37612
- path === "" ? Promise.resolve([]) : new FeedbackStore(store).helpItems(path)
37613
- ]);
37614
- return {
37615
- ...model,
37616
- ...annotation !== null ? { note: annotation.text } : {},
37617
- ...feedback.length > 0 ? { feedback } : {}
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 }
37618
37646
  };
37619
- } catch {
37620
- return model;
37621
- }
37622
- }
37623
- function renderResult(value, rep) {
37624
- const json = JSON.stringify(value ?? null);
37625
- if (rep === "json") {
37626
- return new Response(json, { headers: { "content-type": contentTypeFor("json") } });
37627
- }
37628
- return new Response(`\`\`\`json
37629
- ${json}
37630
- \`\`\`
37631
- `, {
37632
- headers: { "content-type": contentTypeFor("markdown") }
37633
- });
37634
- }
37635
- function renderTreeDsl(tree) {
37636
- const lines = [];
37637
- const walk = (n, depth) => {
37638
- const indent = " ".repeat(depth);
37639
- const label = n.path === "" ? "/" : n.path;
37640
- const trunc = n.truncated ? " \u2026" : "";
37641
- lines.push(`${indent}${label} [${n.kind}] ${n.description}${trunc}`);
37642
- for (const child of n.children ?? []) walk(child, depth + 1);
37643
- };
37644
- walk(tree, 0);
37645
- return `${lines.join("\n")}
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)));
37647
37665
  }
37648
37666
 
37649
37667
  // ../app/src/federation.ts
@@ -37867,6 +37885,24 @@ function assertRemoteConfigAllowed(config, settings) {
37867
37885
  assertRemoteAllowed(baseUrl, settings);
37868
37886
  }
37869
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
+
37870
37906
  // ../app/src/routes/register.ts
37871
37907
  async function authorizeToolNode(c, env, node, encryptionKey) {
37872
37908
  const { deps } = env;
@@ -37923,16 +37959,17 @@ async function handleRegister(c, env) {
37923
37959
  );
37924
37960
  }
37925
37961
  const body = parseNodeInput(raw);
37926
- assertRemoteConfigAllowed(body.config, await resolveRemoteSettings(store, deps.remote));
37927
37962
  if (!check(ctx, path, "register").allow) {
37928
37963
  throw new TBError("permission_denied", `no scope grants 'register' on '${path}'`);
37929
37964
  }
37930
- await assertRegisterPath2(registry, ctx, body.path, "write", deps);
37931
- assertSecretRefUse(ctx.scopes, body.config);
37932
- await assertMcpOAuthConfig(body.config, deps.secrets);
37933
- await assertContextConfig(body.config, deps);
37934
- await assertSkillhubConfig(body.config, deps);
37935
- await assertToolConfig(body.config, deps, ctx, body.path);
37965
+ await assertNodeConfigMutation({
37966
+ action: "write",
37967
+ config: body.config,
37968
+ ctx,
37969
+ deps,
37970
+ registry,
37971
+ targetPath: body.path
37972
+ });
37936
37973
  await searchSync?.ensureSeeded();
37937
37974
  const now = (/* @__PURE__ */ new Date()).toISOString();
37938
37975
  const marker = await searchSync?.markNode(body.path);
@@ -37943,12 +37980,9 @@ async function handleRegister(c, env) {
37943
37980
  await searchSync?.abort(marker);
37944
37981
  throw error;
37945
37982
  }
37946
- await invalidateToolCache(store, body.path);
37947
- await invalidateMcpEra(store, body.path);
37948
- await invalidateMcpOAuth(store, body.path);
37949
- await invalidateProviderOAuth(store, body.path);
37983
+ await invalidateNodeDerivedState(store, body.path);
37950
37984
  await searchSync?.reconcileNodeQuietly(body.path, { marker });
37951
- if (await refreshDynamicSearchNode(node, ctx, deps)) await searchSync?.abort(marker);
37985
+ if (await refreshDynamicSearchNode(node, ctx, deps, searchSync)) await searchSync?.abort(marker);
37952
37986
  return new Response(JSON.stringify(registryNodeSchema.parse(node)), {
37953
37987
  headers: { "content-type": contentTypeFor("json") }
37954
37988
  });
@@ -38045,7 +38079,7 @@ async function handleDescribe(c, env) {
38045
38079
  await assertContextAlive(node, node.config, registry);
38046
38080
  const cfg = node.config;
38047
38081
  const local = cfg.provider !== "r2" && cfg.provider !== "s3" ? localContext(deps, node) : null;
38048
- let capabilities = cfg.provider === "r2" || cfg.provider === "s3" || cfg.provider === "device-fs" || deviceMarkerOf(cfg.providerConfig) !== null ? CONTEXT_CAPABILITIES : local !== null ? localCapabilities(local) : (await requirePluginExport(deps, cfg.provider, "context", "context", cfg.export)).export.capabilities ?? [];
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 ?? [];
38049
38083
  if ((cfg.provider === "r2" || cfg.provider === "s3") && cfg.readOnly !== true && await contextDirectUploadAvailable(cfg, deps)) {
38050
38084
  capabilities = [...capabilities, CONTEXT_DIRECT_UPLOAD_CAPABILITY];
38051
38085
  }
@@ -38713,12 +38747,8 @@ var FederatedSearchSessionStore = class {
38713
38747
  }
38714
38748
  };
38715
38749
 
38716
- // ../app/src/search/localSearch.ts
38717
- function normalizedEffect(effect) {
38718
- return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
38719
- }
38720
- function projectSearchTool(tool, detail) {
38721
- if (detail === "full") return tool;
38750
+ // ../app/src/search/compactTool.ts
38751
+ function compactSearchTool(tool) {
38722
38752
  const compact = { ...tool };
38723
38753
  delete compact.inputSchema;
38724
38754
  delete compact.outputSchema;
@@ -38738,6 +38768,14 @@ function projectSearchTool(tool, detail) {
38738
38768
  }
38739
38769
  return compact;
38740
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
+ }
38741
38779
  async function executeLocalSearch(deps, request) {
38742
38780
  const { ctx, search, searchSync, state } = deps;
38743
38781
  const query = request.query;
@@ -38893,24 +38931,7 @@ function continuationSourceError(error) {
38893
38931
  return retryableContinuationError();
38894
38932
  }
38895
38933
  function compactItem(item) {
38896
- const tool = { ...item.tool };
38897
- delete tool.inputSchema;
38898
- delete tool.outputSchema;
38899
- if (tool.description !== void 0) {
38900
- const encoder = new TextEncoder();
38901
- if (encoder.encode(tool.description).length > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) {
38902
- let description = "";
38903
- let bytes = 0;
38904
- for (const char of tool.description) {
38905
- const size = encoder.encode(char).length;
38906
- if (bytes + size > TOOL_SEARCH_DESCRIPTION_BYTES_MAX) break;
38907
- description += char;
38908
- bytes += size;
38909
- }
38910
- tool.description = description;
38911
- }
38912
- }
38913
- return { ...item, tool };
38934
+ return { ...item, tool: compactSearchTool(item.tool) };
38914
38935
  }
38915
38936
  function normalizedEffect2(effect) {
38916
38937
  return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
@@ -39610,9 +39631,6 @@ async function executeFederatedSearch(deps, input) {
39610
39631
  "recursive search requires explicit TB_INSTANCE_ID and atomic StateStore CAS"
39611
39632
  );
39612
39633
  }
39613
- if ((input.request.opts?.mode ?? "keyword") !== "keyword") {
39614
- throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
39615
- }
39616
39634
  const wireOpts = input.request.opts ?? {};
39617
39635
  const constraints = normalizeToolSearchOptions({
39618
39636
  ...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
@@ -39925,13 +39943,6 @@ function registerSearchRoute(app, env) {
39925
39943
  );
39926
39944
  }
39927
39945
  const request = requestWithNormalizedLimit(parsed.data);
39928
- const mode = request.opts?.mode ?? "keyword";
39929
- if (mode === "semantic" && !capabilities.includes("search:semantic")) {
39930
- throw new TBError(
39931
- "invalid_argument",
39932
- "search mode 'semantic' requires capability 'search:semantic'"
39933
- );
39934
- }
39935
39946
  const explicitFederation = request.opts?.federation;
39936
39947
  const cursor = request.opts?.cursor;
39937
39948
  const federation = explicitFederation ?? (cursor !== void 0 && !isFederatedSearchSessionHandle(cursor) ? "local" : capabilities.includes("search:federated") ? "recursive" : "local");
@@ -39941,9 +39952,6 @@ function registerSearchRoute(app, env) {
39941
39952
  "recursive search requires capability 'search:federated'"
39942
39953
  );
39943
39954
  }
39944
- if (federation === "recursive" && mode === "semantic") {
39945
- throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
39946
- }
39947
39955
  const normalizedRequest = {
39948
39956
  ...request,
39949
39957
  opts: { ...request.opts ?? {}, federation }
@@ -40011,7 +40019,7 @@ async function helpModelFor(node, registry, ctx, builtins, deps, opts) {
40011
40019
  }
40012
40020
  if (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") {
40013
40021
  const provider = await providerFor(node, ctx, deps);
40014
- 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);
40015
40023
  const { exposed } = virtualizeTools(node.virtualize, raw);
40016
40024
  return toolsToHelpModel(
40017
40025
  node.path,
@@ -40081,7 +40089,8 @@ async function commandHelpModelFor(registry, ctx, builtins, deps, path, opts) {
40081
40089
  const full = await helpModelFor(node, registry, ctx, builtins, deps, {
40082
40090
  refresh: opts.refresh,
40083
40091
  schemas: true,
40084
- now: (/* @__PURE__ */ new Date()).toISOString()
40092
+ now: (/* @__PURE__ */ new Date()).toISOString(),
40093
+ searchSync: opts.searchSync
40085
40094
  });
40086
40095
  const cmd = full.cmds.find((c) => c.name === command);
40087
40096
  if (cmd === void 0) return null;
@@ -40127,20 +40136,8 @@ var toolSpecCommand = (node, tool, providerBacked = false) => {
40127
40136
  ...tool.confirm === true ? { confirm: true } : {}
40128
40137
  };
40129
40138
  };
40130
- var remoteCommand = (localNodePath, model, command) => {
40131
- const remoteNodePath = model.node.path.replace(/^\/+|\/+$/g, "");
40132
- const remoteCommandPath = command.path.replace(/^\/+|\/+$/g, "");
40133
- if (remoteCommandPath !== remoteNodePath && !remoteCommandPath.startsWith(`${remoteNodePath}/`)) {
40134
- throw new TBError("unavailable", "remote ~help returned a command outside its node");
40135
- }
40136
- const suffix = remoteCommandPath.slice(remoteNodePath.length);
40137
- return mcpCommand(localNodePath, model.node.description, {
40138
- ...command,
40139
- path: `/${localNodePath}${suffix}`
40140
- });
40141
- };
40142
40139
  function mcpBridgeFor(c, env, app) {
40143
- const { builtinsOf, deps, globalSearchCapabilities } = env;
40140
+ const { builtinsOf, deps, globalSearchCapabilities, searchSync } = env;
40144
40141
  const ctx = c.get("ctx");
40145
40142
  const registry = new NodeRegistryStore(c.get("store"));
40146
40143
  let remoteRequests = 0;
@@ -40159,7 +40156,7 @@ function mcpBridgeFor(c, env, app) {
40159
40156
  additionalProperties: false,
40160
40157
  properties: {
40161
40158
  query: { type: "string", minLength: 1 },
40162
- mode: { type: "string", enum: ["keyword", "semantic"] },
40159
+ mode: { type: "string", enum: ["keyword"] },
40163
40160
  limit: { type: "integer", minimum: 1, maximum: 200 },
40164
40161
  cursor: { type: "string", minLength: 1 },
40165
40162
  detail: { type: "string", enum: ["compact", "full"], default: "compact" },
@@ -40227,6 +40224,10 @@ function mcpBridgeFor(c, env, app) {
40227
40224
  };
40228
40225
  const remoteHelp = async (path) => {
40229
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
+ }
40230
40231
  const headers = new Headers(c.req.raw.headers);
40231
40232
  headers.set("accept", "application/json");
40232
40233
  const response = await remotePassthroughIfMatch(
@@ -40241,28 +40242,11 @@ function mcpBridgeFor(c, env, app) {
40241
40242
  if (response === null || !response.ok) {
40242
40243
  throw new TBError("unavailable", `remote ~help failed for '${path}'`, { retryable: true });
40243
40244
  }
40244
- const model = await response.json().catch(() => null);
40245
- if (model === null || !Array.isArray(model.cmds) || typeof model.node?.path !== "string") {
40245
+ const parsed = helpJsonSchema.safeParse(await response.json().catch(() => null));
40246
+ if (!parsed.success) {
40246
40247
  throw new TBError("unavailable", `remote ~help returned invalid JSON for '${path}'`);
40247
40248
  }
40248
- const owner = await registry.resolve(path).catch(() => null);
40249
- if (owner?.node.kind !== "remote") {
40250
- throw remoteProtocolError(`remote ~help path '${path}' lost its mount owner`);
40251
- }
40252
- const modelPath = canonicalRemotePath(model.node.path, true);
40253
- if (modelPath !== owner.rest) {
40254
- throw remoteProtocolError(`remote ~help path '${modelPath}' does not match request`);
40255
- }
40256
- for (const command of model.cmds) {
40257
- if (!command.path.startsWith("/") || command.path.startsWith("//")) {
40258
- throw remoteProtocolError(`remote ~help returned invalid command path '${command.path}'`);
40259
- }
40260
- const commandPath = canonicalRemotePath(command.path.slice(1), true);
40261
- if (!remotePathWithin(modelPath, commandPath)) {
40262
- throw remoteProtocolError(`remote ~help command '${command.path}' escapes its node`);
40263
- }
40264
- }
40265
- return model;
40249
+ return projector.projectHelp(parsed.data, path, ctx);
40266
40250
  };
40267
40251
  const remotePaths = async (root) => {
40268
40252
  const found = [];
@@ -40311,13 +40295,10 @@ function mcpBridgeFor(c, env, app) {
40311
40295
  if (check(ctx, path, command.scope).allow) {
40312
40296
  let detailed = command;
40313
40297
  if (command.inputSchema === void 0 && command.path !== `/${model2.node.path}`) {
40314
- const remoteNodePath = model2.node.path.replace(/^\/+|\/+$/g, "");
40315
- const remoteCommandPath = command.path.replace(/^\/+|\/+$/g, "");
40316
- const detailPath = `${path}${remoteCommandPath.slice(remoteNodePath.length)}`;
40317
- const detail = await remoteHelp(detailPath);
40298
+ const detail = await remoteHelp(command.path.replace(/^\/+/, ""));
40318
40299
  detailed = detail.cmds.find((item) => item.name === command.name) ?? command;
40319
40300
  }
40320
- result.push(remoteCommand(path, model2, detailed));
40301
+ result.push(mcpCommand(path, model2.node.description, detailed));
40321
40302
  }
40322
40303
  }
40323
40304
  }
@@ -40333,7 +40314,7 @@ function mcpBridgeFor(c, env, app) {
40333
40314
  if ((node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config !== void 0) {
40334
40315
  if (!check(ctx, node.path, "call").allow) continue;
40335
40316
  const provider = await providerFor(node, ctx, deps);
40336
- const raw = await upstreamTools(node, provider, deps, false, now);
40317
+ const raw = await upstreamTools(node, provider, deps, false, now, searchSync);
40337
40318
  const { exposed } = virtualizeTools(node.virtualize, raw);
40338
40319
  result.push(...exposed.map((tool) => toolSpecCommand(node, tool, true)));
40339
40320
  continue;
@@ -40341,7 +40322,8 @@ function mcpBridgeFor(c, env, app) {
40341
40322
  const model = await helpModelFor(node, registry, ctx, builtinsOf(c.get("store")), deps, {
40342
40323
  includeDirectUpload: check(ctx, node.path, "write").allow,
40343
40324
  refresh: false,
40344
- now
40325
+ now,
40326
+ searchSync
40345
40327
  });
40346
40328
  for (const command of model.cmds) {
40347
40329
  if (check(ctx, node.path, command.scope).allow) {
@@ -40444,7 +40426,7 @@ function mcpBridgeFor(c, env, app) {
40444
40426
  throw TBError.notFound("not found");
40445
40427
  }
40446
40428
  const provider = await providerFor(node, ctx, deps);
40447
- 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);
40448
40430
  const upstreamName = resolveUpstreamTool(node.virtualize, raw, tool.toolName);
40449
40431
  const result2 = await provider.call(upstreamName, args);
40450
40432
  return {
@@ -40606,7 +40588,7 @@ async function handleInvoke(c, env) {
40606
40588
  }
40607
40589
  const args2 = await readInvokeBody();
40608
40590
  const provider = await providerFor(node, ctx, deps);
40609
- 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);
40610
40592
  const upstreamName = resolveUpstreamTool(node.virtualize, tools, command);
40611
40593
  const result2 = await provider.call(upstreamName, args2);
40612
40594
  return renderResult(result2.content, negotiate(c.req.header("accept")));
@@ -40670,7 +40652,7 @@ async function handleInvoke(c, env) {
40670
40652
  if (cfg.provider !== "r2" && cfg.provider !== "s3") {
40671
40653
  const local = localContext(deps, node);
40672
40654
  if (local !== null) {
40673
- const result4 = await dispatchContextCmd2(local, command, args2);
40655
+ const result4 = await dispatchContextCmd(local, command, args2);
40674
40656
  return renderResult(result4, negotiate(c.req.header("accept")));
40675
40657
  }
40676
40658
  const { manifest, export: exported } = await requirePluginExport(
@@ -40690,11 +40672,11 @@ async function handleInvoke(c, env) {
40690
40672
  ...cfg.authRef !== void 0 ? { upstreamAuthRef: cfg.authRef } : {},
40691
40673
  ...deps.pluginBindings !== void 0 ? { bindings: deps.pluginBindings } : {}
40692
40674
  });
40693
- const result3 = await dispatchContextCmd2(provider2, command, args2);
40675
+ const result3 = await dispatchContextCmd(provider2, command, args2);
40694
40676
  return renderResult(result3, negotiate(c.req.header("accept")));
40695
40677
  }
40696
40678
  const provider = await contextProviderFor(node, cfg, deps, c.req.url);
40697
- const result2 = await dispatchContextCmd2(provider, command, args2);
40679
+ const result2 = await dispatchContextCmd(provider, command, args2);
40698
40680
  return renderResult(result2, negotiate(c.req.header("accept")));
40699
40681
  }
40700
40682
  if (node.kind === "skillhub" && node.config?.kind === "skillhub") {
@@ -40712,7 +40694,7 @@ async function handleInvoke(c, env) {
40712
40694
  throw new TBError("permission_denied", `readOnly \u6302\u8F7D\u62D2\u7EDD '${command}'`);
40713
40695
  }
40714
40696
  const provider = await skillhubProviderFor(node, cfg, deps, c.req.url);
40715
- const result2 = await dispatchSkillhubCmd2(provider, command, args2);
40697
+ const result2 = await dispatchSkillhubCmd(provider, command, args2);
40716
40698
  return renderResult(result2, negotiate(c.req.header("accept")));
40717
40699
  }
40718
40700
  if (node.kind !== "builtin" || node.config?.kind !== "builtin") {
@@ -40736,19 +40718,14 @@ async function handleInvoke(c, env) {
40736
40718
  throw new TBError("invalid_argument", "field 'path' must be a string");
40737
40719
  }
40738
40720
  const cfgPatch = cmd === "write" ? args.config : cmd === "update" ? args.patch?.config : void 0;
40739
- assertRemoteConfigAllowed(cfgPatch, await resolveRemoteSettings(store, deps.remote));
40740
- await assertRegisterPath2(
40741
- registry,
40721
+ await assertNodeConfigMutation({
40722
+ action: cmd === "delete" ? "delete" : "write",
40723
+ config: cfgPatch,
40742
40724
  ctx,
40743
- targetPath,
40744
- cmd === "delete" ? "delete" : "write",
40745
- deps
40746
- );
40747
- assertSecretRefUse(ctx.scopes, cfgPatch);
40748
- await assertMcpOAuthConfig(cfgPatch, deps.secrets);
40749
- await assertContextConfig(cfgPatch, deps);
40750
- await assertSkillhubConfig(cfgPatch, deps);
40751
- await assertToolConfig(cfgPatch, deps, ctx, targetPath);
40725
+ deps,
40726
+ registry,
40727
+ targetPath
40728
+ });
40752
40729
  registryTarget = targetPath;
40753
40730
  }
40754
40731
  if (registryTarget !== void 0 && (cmd === "write" || cmd === "update")) {
@@ -40778,22 +40755,16 @@ async function handleInvoke(c, env) {
40778
40755
  return tbErrorResponse(new TBError("internal", "internal error"));
40779
40756
  }
40780
40757
  if (registryTarget !== void 0) {
40781
- await invalidateToolCache(store, registryTarget);
40782
- await invalidateMcpEra(store, registryTarget);
40783
- await invalidateMcpOAuth(store, registryTarget);
40784
- await invalidateProviderOAuth(store, registryTarget);
40758
+ await invalidateNodeDerivedState(store, registryTarget);
40785
40759
  await searchSync?.reconcileNodeQuietly(registryTarget, { marker: registryMarker });
40786
40760
  const current = await registry.get(registryTarget).catch(() => null);
40787
- if (current !== null && await refreshDynamicSearchNode(current, ctx, deps)) {
40761
+ if (current !== null && await refreshDynamicSearchNode(current, ctx, deps, searchSync)) {
40788
40762
  await searchSync?.abort(registryMarker);
40789
40763
  }
40790
40764
  }
40791
40765
  const pluginEmptyPaths = [];
40792
40766
  for (const mount of pluginMounts) {
40793
- await invalidateToolCache(store, mount.node.path);
40794
- await invalidateMcpEra(store, mount.node.path);
40795
- await invalidateMcpOAuth(store, mount.node.path);
40796
- await invalidateProviderOAuth(store, mount.node.path);
40767
+ await invalidateNodeDerivedState(store, mount.node.path);
40797
40768
  const providerId = mount.node.config?.kind === "tool" ? mount.node.config.provider : "";
40798
40769
  const manifest = await store.get(KEY_PLUGIN + providerId);
40799
40770
  if (manifest !== null && manifest.enabled !== false) {
@@ -40841,8 +40812,7 @@ function createRouteEnv(deps) {
40841
40812
  if (!declared.has("search")) return [];
40842
40813
  return [
40843
40814
  "search",
40844
- ...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : [],
40845
- ...declared.has("search:semantic") ? ["search:semantic"] : []
40815
+ ...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : []
40846
40816
  ];
40847
40817
  };
40848
40818
  let catalogDigest;
@@ -40862,7 +40832,7 @@ function createRouteEnv(deps) {
40862
40832
 
40863
40833
  // ../app/src/routes/help.ts
40864
40834
  async function handleHelp(c, env) {
40865
- const { builtinsOf, deps } = env;
40835
+ const { builtinsOf, deps, searchSync } = env;
40866
40836
  const path = splitReserved(new URL(c.req.url).pathname, "~help");
40867
40837
  if (path === null) throw TBError.notFound("no such path");
40868
40838
  const ctx = c.get("ctx");
@@ -40901,11 +40871,12 @@ async function handleHelp(c, env) {
40901
40871
  try {
40902
40872
  node = await registry.get(path);
40903
40873
  } catch {
40904
- const toolModel = await toolHelpModelFor(c, ctx, registry, path, deps);
40874
+ const toolModel = await toolHelpModelFor(c, ctx, registry, path, deps, searchSync);
40905
40875
  if (toolModel !== null) return renderHelp(await enrichHelp(toolModel, path, store), rep);
40906
40876
  const cmdModel = await commandHelpModelFor(registry, ctx, builtins, deps, path, {
40907
40877
  refresh,
40908
- schemas: schemas2
40878
+ schemas: schemas2,
40879
+ searchSync
40909
40880
  });
40910
40881
  if (cmdModel !== null) return renderHelp(await enrichHelp(cmdModel, path, store), rep);
40911
40882
  throw TBError.notFound("not found");
@@ -40913,7 +40884,8 @@ async function handleHelp(c, env) {
40913
40884
  const model = await helpModelFor(node, registry, ctx, builtins, deps, {
40914
40885
  refresh,
40915
40886
  schemas: schemas2,
40916
- now: (/* @__PURE__ */ new Date()).toISOString()
40887
+ now: (/* @__PURE__ */ new Date()).toISOString(),
40888
+ searchSync
40917
40889
  });
40918
40890
  return renderHelp(await enrichHelp(model, path, store), rep);
40919
40891
  }
@@ -41021,6 +40993,7 @@ function createTbApp(deps) {
41021
40993
  });
41022
40994
  registerMcpRoute(app, env);
41023
40995
  registerSearchRoute(app, env);
40996
+ registerDeviceMailboxRoutes(app, env);
41024
40997
  app.get(
41025
40998
  "/system/device/ws",
41026
40999
  (c) => runHandler(async () => {
@@ -41061,13 +41034,6 @@ function createTbApp(deps) {
41061
41034
  async (c) => await runHandler(async () => {
41062
41035
  const segs = new URL(c.req.url).pathname.replace(/\/+$/, "").split("/");
41063
41036
  const last = segs.pop() ?? "";
41064
- const pathname = new URL(c.req.url).pathname.replace(/\/+$/, "");
41065
- if (pathname.startsWith("/~device/operations/")) {
41066
- return await handleDeviceMailboxControl(c, env);
41067
- }
41068
- if (pathname.startsWith("/~device/mailbox/")) {
41069
- return await handleDeviceMailboxData(c, env);
41070
- }
41071
41037
  if (last === "~register") return await handleRegister(c, env);
41072
41038
  if (last === "~authorize") return await handleAuthorize(c, env);
41073
41039
  if (last === "~feedback" || segs[segs.length - 1] === "~feedback") {
@@ -41178,17 +41144,12 @@ var DeviceHub = class {
41178
41144
  };
41179
41145
  const conn = this.activeByDevice.get(deviceId);
41180
41146
  if (conn === void 0) return offline;
41181
- const authCtx = await identify(this.store, conn.authorization, (/* @__PURE__ */ new Date()).toISOString());
41182
- const authorized = authCtx !== null && authCtx.keyId === conn.keyId && conn.mountPath !== void 0 && checkRegisterPath({
41183
- sk: {
41184
- scopes: authCtx.scopes,
41185
- id: authCtx.keyId,
41186
- ...authCtx.registerPaths !== void 0 ? { registerPaths: authCtx.registerPaths } : {}
41187
- },
41188
- targetPath: conn.mountPath,
41189
- action: "write",
41190
- existing: null
41191
- }).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
+ });
41192
41153
  if (!authorized) {
41193
41154
  conn.session.reject(TBError.unauthenticated());
41194
41155
  return offline;
@@ -41220,10 +41181,11 @@ var DeviceHub = class {
41220
41181
  }
41221
41182
  if (meta.disconnectedAt === void 0) {
41222
41183
  const now = (/* @__PURE__ */ new Date()).toISOString();
41223
- try {
41224
- await new NodeRegistryStore(this.store).setOnline(meta.mountPath, false, now);
41225
- } catch {
41226
- }
41184
+ await markDeviceDisconnected({
41185
+ mountPath: meta.mountPath,
41186
+ now,
41187
+ registry: new NodeRegistryStore(this.store)
41188
+ });
41227
41189
  await this.store.put(KEY_DEVICE_META + meta.deviceId, { ...meta, disconnectedAt: now });
41228
41190
  this.scheduleReclaim(meta.deviceId, this.reclaimSec * 1e3);
41229
41191
  } else {
@@ -41321,9 +41283,7 @@ var DeviceHub = class {
41321
41283
  ...this.search === void 0 ? {} : { search: this.search }
41322
41284
  });
41323
41285
  if (!searchIndexed) {
41324
- console.warn(
41325
- `[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.`
41326
- );
41286
+ console.warn(deviceSearchCapacityWarning(conn.deviceId, mountPath));
41327
41287
  }
41328
41288
  const meta = { deviceId: conn.deviceId, mountPath, keyId, connectedAt: now };
41329
41289
  await this.store.put(KEY_DEVICE_META + conn.deviceId, meta);
@@ -41348,10 +41308,11 @@ var DeviceHub = class {
41348
41308
  const raw = await this.store.get(KEY_DEVICE_META + conn.deviceId);
41349
41309
  if (!isDeviceMeta(raw)) return;
41350
41310
  const now = (/* @__PURE__ */ new Date()).toISOString();
41351
- try {
41352
- await new NodeRegistryStore(this.store).setOnline(raw.mountPath, false, now);
41353
- } catch {
41354
- }
41311
+ await markDeviceDisconnected({
41312
+ mountPath: raw.mountPath,
41313
+ now,
41314
+ registry: new NodeRegistryStore(this.store)
41315
+ });
41355
41316
  await this.store.put(KEY_DEVICE_META + conn.deviceId, { ...raw, disconnectedAt: now });
41356
41317
  this.scheduleReclaim(conn.deviceId, this.reclaimSec * 1e3);
41357
41318
  }
@@ -41384,13 +41345,12 @@ var DeviceHub = class {
41384
41345
  }
41385
41346
  const raw = await this.store.get(KEY_DEVICE_META + deviceId);
41386
41347
  if (!isDeviceMeta(raw)) return;
41387
- const searchSync = this.search === void 0 ? void 0 : new SearchSynchronizer(this.store, this.search);
41388
- const marker = await searchSync?.markSubtree(raw.mountPath);
41389
- try {
41390
- await new NodeRegistryStore(this.store).deleteSubtree(raw.mountPath);
41391
- } catch {
41392
- }
41393
- 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
+ });
41394
41354
  await this.store.delete(KEY_DEVICE_META + deviceId);
41395
41355
  }
41396
41356
  /** ws 协议层探活:上一轮未回 pong → terminate(触发 close → markDisconnected)。 */
@@ -42481,7 +42441,7 @@ import { join as join4 } from "path";
42481
42441
  // package.json
42482
42442
  var package_default = {
42483
42443
  name: "@tool-bridge/server",
42484
- version: "0.20.0",
42444
+ version: "0.21.0",
42485
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",
42486
42446
  type: "module",
42487
42447
  license: "MIT",