@tool-bridge/server 0.20.0 → 0.21.1

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 (111) hide show
  1. package/dist/{airtable-ZNWHIJMO.js → airtable-LLZDFN7M.js} +19 -16
  2. package/dist/{alt_text_generator_ai-X7RMDDM5.js → alt_text_generator_ai-W7O34SNG.js} +4 -4
  3. package/dist/{amap-3VER75VH.js → amap-VOX4T2AW.js} +4 -4
  4. package/dist/{anthropic-N7NTB3UC.js → anthropic-G23NN3X5.js} +23 -25
  5. package/dist/{apify-JB5P5AJI.js → apify-Y3SS734K.js} +20 -19
  6. package/dist/{brave_search-USL5OOBS.js → brave_search-ZC7OAPSZ.js} +16 -14
  7. package/dist/{chunk-6ANLYNFB.js → chunk-ATK45BXV.js} +1 -1
  8. package/dist/{chunk-6X55ALDX.js → chunk-E6JCVKW6.js} +6 -5
  9. package/dist/{chunk-RVUERWUZ.js → chunk-KMTT3I6D.js} +768 -1048
  10. package/dist/chunk-R6P4XAE5.js +248 -0
  11. package/dist/{chunk-3WL6EXYX.js → chunk-U7B6DS2O.js} +1 -1
  12. package/dist/{chunk-WIGQCP6O.js → chunk-UZM2RXZH.js} +159 -182
  13. package/dist/chunk-XYBU3WZH.js +71 -0
  14. package/dist/{circleci-LBGB25LP.js → circleci-3BAX5FCG.js} +20 -24
  15. package/dist/{clerk-6RX37CNZ.js → clerk-6LEM6TNU.js} +22 -21
  16. package/dist/{cloudflare_dns-NQGW7FHV.js → cloudflare_dns-KINYTJUD.js} +21 -17
  17. package/dist/{cohere-XPC2NAEW.js → cohere-XCJ5AJ6B.js} +19 -17
  18. package/dist/{confluence-ZUG2LT5A.js → confluence-B3EWPZVE.js} +16 -11
  19. package/dist/{convertapi-ZLXKWV3L.js → convertapi-EUWSQYPF.js} +3 -3
  20. package/dist/{currencyapi-T3A3HSOD.js → currencyapi-KVTIK3GL.js} +23 -26
  21. package/dist/{deepseek-NJMOD23E.js → deepseek-PC5YGNC5.js} +27 -17
  22. package/dist/{docker_hub-D2MQCSJ7.js → docker_hub-C3MA5FU6.js} +4 -4
  23. package/dist/{dropbox-MI6U3SOT.js → dropbox-XBV7Z2IR.js} +9 -8
  24. package/dist/{dub-P2BYBYV4.js → dub-LDOFG3HC.js} +22 -19
  25. package/dist/{e2b-BGKIAE54.js → e2b-JMCPSVBW.js} +20 -20
  26. package/dist/{exa-EUY7VV4C.js → exa-RAK7O7RD.js} +22 -25
  27. package/dist/{fal_ai-AAOG5FYJ.js → fal_ai-23E7JPNT.js} +18 -17
  28. package/dist/feishu-7QRIDUV6.js +12019 -0
  29. package/dist/{feishu_custom_bot-BWGMNVXF.js → feishu_custom_bot-TJ4K7QNJ.js} +3 -3
  30. package/dist/{figma-SJSWYCGD.js → figma-E6IX5DTH.js} +21 -23
  31. package/dist/{firehydrant-MOXK2PST.js → firehydrant-ZBLPQREE.js} +15 -15
  32. package/dist/{fixer-ISIDUDZU.js → fixer-BWON27UO.js} +4 -4
  33. package/dist/{fly-U4KPSGBF.js → fly-ZH3HFCZJ.js} +20 -16
  34. package/dist/{front-ZHM2YX2E.js → front-QCGXYEDJ.js} +20 -23
  35. package/dist/{geocodio-SVCCAXAV.js → geocodio-YGNI7DA4.js} +4 -4
  36. package/dist/{ghost-INTR7FIY.js → ghost-EVSGKH2Q.js} +4 -4
  37. package/dist/{github-T275VVWR.js → github-MA2BGRCJ.js} +28 -27
  38. package/dist/{gmail-KSSPOE67.js → gmail-E5E6AADE.js} +17 -17
  39. package/dist/{googlecalendar-JB5RW27L.js → googlecalendar-BTLFPXTD.js} +22 -19
  40. package/dist/{googledocs-G522PUGW.js → googledocs-BUHL4J2H.js} +6 -5
  41. package/dist/{grafana-44ZPIV5P.js → grafana-GLU2DWF2.js} +24 -25
  42. package/dist/{graphhopper-2MORXLOW.js → graphhopper-WSMB3TCD.js} +4 -4
  43. package/dist/index.d.ts +31 -30
  44. package/dist/index.js +4 -3
  45. package/dist/{ip2proxy-R4K4KEMI.js → ip2proxy-EZ64Y4ZH.js} +4 -4
  46. package/dist/{ipgeolocation_io-H4J55OGI.js → ipgeolocation_io-AA7HAS22.js} +4 -4
  47. package/dist/{ipqualityscore-XGID4OHA.js → ipqualityscore-BHJXVTJT.js} +4 -4
  48. package/dist/{jina_ai-7ZVDOR3Z.js → jina_ai-EFBHTBXL.js} +20 -21
  49. package/dist/{jira-CTIYZTT3.js → jira-HQSHAA5K.js} +20 -16
  50. package/dist/{langsmith-ISP567FS.js → langsmith-5W6SCT6F.js} +25 -25
  51. package/dist/{lemlist-OGKK272F.js → lemlist-HJWH6UMK.js} +23 -24
  52. package/dist/{linear-6JX4E3JI.js → linear-K7RYFYAD.js} +6 -5
  53. package/dist/{logsnag-BVB36ZCL.js → logsnag-4KFOHL37.js} +20 -22
  54. package/dist/main.js +103 -102
  55. package/dist/{mattermost-UECVPZGY.js → mattermost-S2BRCWLQ.js} +21 -17
  56. package/dist/{mem0-EUA7S2OO.js → mem0-CAW43CIC.js} +15 -15
  57. package/dist/{memos-BYSBJRWZ.js → memos-TWEGPKFM.js} +39 -34
  58. package/dist/{metabase-4JXZH3YG.js → metabase-MMSEPCOF.js} +22 -18
  59. package/dist/{mistral_ai-2C5BTBKW.js → mistral_ai-64BGZQHP.js} +24 -21
  60. package/dist/{mother_duck-ZGNNNR3X.js → mother_duck-H7PCQNZ2.js} +23 -19
  61. package/dist/{ngrok-NTPGMRXI.js → ngrok-GQCUGZKJ.js} +17 -22
  62. package/dist/{notes-KGYXX6N6.js → notes-PFMKRO33.js} +1 -1
  63. package/dist/{notion-EX3X2G7G.js → notion-DQWZWP4N.js} +20 -17
  64. package/dist/{open_exchange_rates-2V2LIGPS.js → open_exchange_rates-QMMD735E.js} +4 -4
  65. package/dist/{openai-KQFQPK7Z.js → openai-W6PX32NZ.js} +22 -16
  66. package/dist/{opengraph_io-B63SEYJY.js → opengraph_io-HNDBXYNW.js} +4 -4
  67. package/dist/{openrouter-62A4H62Q.js → openrouter-JJYMEAUL.js} +19 -14
  68. package/dist/{outline-QC33XLCK.js → outline-MCBNQZYY.js} +18 -20
  69. package/dist/{paddle-HDZG35Y7.js → paddle-D2SKLUDN.js} +17 -15
  70. package/dist/{perplexity-M7LOMJWA.js → perplexity-XIFMKUAE.js} +19 -23
  71. package/dist/{pinecone-Y7YCJRZM.js → pinecone-HUFTALV2.js} +19 -24
  72. package/dist/{pipedrive-FLJA62UX.js → pipedrive-Y2O7QGFF.js} +22 -23
  73. package/dist/{polar-AE55Q425.js → polar-TYTOMOSR.js} +19 -17
  74. package/dist/{posthog-IAY5D3YW.js → posthog-43JAR3VW.js} +29 -24
  75. package/dist/{postmark-NMKAXDXZ.js → postmark-KL2QJYSG.js} +19 -17
  76. package/dist/{prerender-CDJM7I7N.js → prerender-MENYM2TY.js} +4 -4
  77. package/dist/{pubmed-N437ZJOR.js → pubmed-GOXUVVH5.js} +6 -5
  78. package/dist/{railway-NBWYXCUM.js → railway-V2SID4WV.js} +3 -3
  79. package/dist/{readwise-QRO3UC3R.js → readwise-CI4GIMMJ.js} +16 -15
  80. package/dist/{render-NEW7SROG.js → render-SUBJ6YRZ.js} +21 -20
  81. package/dist/{replicate-73YQLRTY.js → replicate-756MXPYX.js} +20 -21
  82. package/dist/{resend-R5BAZFHZ.js → resend-ZKH3BTBI.js} +15 -11
  83. package/dist/{rootly-5BLALPCS.js → rootly-CD2LJHPI.js} +19 -18
  84. package/dist/{scrapfly-EHFLNNUZ.js → scrapfly-NSRHFDIQ.js} +4 -4
  85. package/dist/{scrapingbee-UXLYS72S.js → scrapingbee-OQHLAVEY.js} +4 -4
  86. package/dist/{screenshot_fyi-KC3NLTJ6.js → screenshot_fyi-DVWG3AXC.js} +4 -4
  87. package/dist/{semantic_scholar-SA5OIUWA.js → semantic_scholar-T4A252KW.js} +21 -20
  88. package/dist/{sentry-BAE3XLJN.js → sentry-546MNDJG.js} +22 -19
  89. package/dist/{shodan-FJR6MKKW.js → shodan-C3OON3Z3.js} +4 -4
  90. package/dist/{shopify-7OA6SLN5.js → shopify-Y2HN2MCQ.js} +23 -19
  91. package/dist/{stripe-MUWDZGGF.js → stripe-VE3CD3AY.js} +3 -3
  92. package/dist/{supabase-SWWGNWYR.js → supabase-U2SL4D63.js} +18 -17
  93. package/dist/{tavily-XQNJ5EDI.js → tavily-KCZHK6HE.js} +20 -20
  94. package/dist/{telegram-RR6JPZOG.js → telegram-3URFBCMY.js} +4 -4
  95. package/dist/{telnyx-7PKXOWSO.js → telnyx-CXQ4EZUX.js} +19 -19
  96. package/dist/{todoist-63X5IUJE.js → todoist-HNPWNYTD.js} +18 -17
  97. package/dist/{trello-MOG63DAI.js → trello-3W2MIXRP.js} +4 -4
  98. package/dist/{turso-ENWTSHDL.js → turso-2EQF7C2Z.js} +23 -30
  99. package/dist/{twilio-FDXUN7VX.js → twilio-4PDBJKWX.js} +6 -5
  100. package/dist/{umami-TM7PROWF.js → umami-5REN7TGP.js} +23 -28
  101. package/dist/{unsplash-NWXLKSOW.js → unsplash-EIXL424F.js} +19 -18
  102. package/dist/{upstash_redis-CUTQ5L2N.js → upstash_redis-G4LQBYHA.js} +17 -13
  103. package/dist/{uptimerobot-NNO32M3I.js → uptimerobot-KNJ3EYLY.js} +3 -3
  104. package/dist/{v2ex-24KMSWS7.js → v2ex-RHJ234PZ.js} +35 -18
  105. package/dist/{vercel-B45LPF6I.js → vercel-AS6NCIC7.js} +21 -18
  106. package/dist/{wolfram_alpha_api-DKJWVR6G.js → wolfram_alpha_api-3G4PUJWB.js} +4 -4
  107. package/dist/{wordpress-XYFSDZXY.js → wordpress-KNDAAUV7.js} +21 -17
  108. package/dist/{workos-BEM7EPHM.js → workos-R6UITSJ7.js} +26 -27
  109. package/dist/{zhihu-CFHZ4NC7.js → zhihu-AGK4HILK.js} +21 -21
  110. package/package.json +5 -5
  111. package/dist/feishu-AN5IJYOW.js +0 -231
@@ -1,6 +1,10 @@
1
+ import {
2
+ EventSourceParserStream,
3
+ pkceChallenge
4
+ } from "./chunk-R6P4XAE5.js";
1
5
  import {
2
6
  createGuardedFetch
3
- } from "./chunk-3WL6EXYX.js";
7
+ } from "./chunk-U7B6DS2O.js";
4
8
  import {
5
9
  AnnotationStore,
6
10
  CONTEXT_CAPABILITIES,
@@ -55,6 +59,7 @@ import {
55
59
  any,
56
60
  appendVia,
57
61
  array,
62
+ assertNoCollision,
58
63
  assertPluginPayloadSize,
59
64
  assertPresignTtlSec,
60
65
  assertSecretRefUse,
@@ -171,7 +176,7 @@ import {
171
176
  resolveIntegration,
172
177
  resolveUpstreamTool,
173
178
  rewriteRemotePath,
174
- safeParse,
179
+ safeParse2 as safeParse,
175
180
  selectFeedbackSearchText,
176
181
  serializeToolSearchSnapshot,
177
182
  sha256Hex,
@@ -196,7 +201,7 @@ import {
196
201
  url,
197
202
  validatePath,
198
203
  virtualizeTools
199
- } from "./chunk-WIGQCP6O.js";
204
+ } from "./chunk-UZM2RXZH.js";
200
205
 
201
206
  // src/config.ts
202
207
  var DEFAULT_PORT = 8787;
@@ -821,300 +826,48 @@ function createS3ObjectStore(cfg, opts) {
821
826
  };
822
827
  }
823
828
 
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
- }
829
+ // ../app/src/deps.ts
830
+ var TOOL_CACHE_TTL_DEFAULT = 300;
864
831
 
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);
832
+ // ../app/src/cursorCleanup.ts
833
+ function positiveInt(value, fallback, field) {
834
+ const actual = value ?? fallback;
835
+ if (!Number.isSafeInteger(actual) || actual < 1) {
836
+ throw new TBError("invalid_argument", `${field} must be a positive integer`);
902
837
  }
903
- return result;
904
- }
905
- function documentsFor(path, tools, feedback) {
906
- return tools.map((tool) => ({ path, tool, ...feedback === "" ? {} : { feedback } }));
838
+ return actual;
907
839
  }
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
- }
840
+ async function runCursorCleanup(opts) {
841
+ const { state } = opts;
842
+ if (state.compareAndSwap === void 0) {
843
+ throw new TBError("unavailable", "cursor cleanup requires StateStore.compareAndSwap");
1100
844
  }
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]);
845
+ const raw = await state.get(opts.progressKey);
846
+ let progress = raw === null ? null : opts.parseProgress(raw);
847
+ let cursor = progress?.cursor;
848
+ for (let pageNumber = 0; pageNumber < opts.maxPages; pageNumber++) {
849
+ const nextCursor = await opts.runPage(cursor, pageNumber);
850
+ if (nextCursor === void 0) {
851
+ if (progress !== null) {
852
+ await state.compareAndSwap(opts.progressKey, progress.revision, null);
1110
853
  }
1111
- } catch {
854
+ return void 0;
1112
855
  }
856
+ const next = {
857
+ cursor: nextCursor,
858
+ revision: (progress?.revision ?? 0) + 1
859
+ };
860
+ const advanced = await state.compareAndSwap(
861
+ opts.progressKey,
862
+ progress?.revision ?? null,
863
+ opts.encodeProgress(next)
864
+ );
865
+ if (!advanced) return void 0;
866
+ progress = next;
867
+ cursor = next.cursor;
1113
868
  }
1114
- };
1115
-
1116
- // ../app/src/deps.ts
1117
- var TOOL_CACHE_TTL_DEFAULT = 300;
869
+ return cursor;
870
+ }
1118
871
 
1119
872
  // ../app/src/storeRefToken.ts
1120
873
  async function hmacKey(secret) {
@@ -1173,13 +926,6 @@ var STORE_READ_TTL_SEC_DEFAULT = 15 * 60;
1173
926
  var STORE_CALL_MAX_OBJECTS_DEFAULT = 4;
1174
927
  var STORE_CALL_MAX_OBJECT_BYTES_DEFAULT = 256 * 1024 * 1024;
1175
928
  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
929
  function randomSecret() {
1184
930
  const bytes = crypto.getRandomValues(new Uint8Array(32));
1185
931
  let out = "";
@@ -1512,44 +1258,28 @@ async function cleanupDefaultStore(deps, opts = {}) {
1512
1258
  const store = await defaultStoreRuntime(deps);
1513
1259
  const maxPages = Math.min(positiveInt(opts.maxPages, 8, "maxPages"), 64);
1514
1260
  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
1261
  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
- }
1262
+ const cursors = await runCursorCleanup({
1263
+ encodeProgress: (progress) => ({ cursors: progress.cursor, revision: progress.revision }),
1264
+ maxPages,
1265
+ parseProgress: (value) => {
1266
+ const progress = cleanupProgress(value);
1267
+ return { cursor: progress.cursors, revision: progress.revision };
1268
+ },
1269
+ progressKey: KEY_STORE_CLEANUP_PROGRESS,
1270
+ runPage: async (cursor, pageNumber) => {
1271
+ const page = await store.service.cleanup({
1272
+ ...limit === void 0 ? {} : { limit },
1273
+ ...cursor === void 0 ? {} : { cursors: cursor },
1274
+ // staging 不使用权威状态 cursor;同一 host tick 只运行一次,避免
1275
+ // maxPages 内重复扫描同一 driver maintenance 范围。
1276
+ runDriverMaintenance: pageNumber === 0
1277
+ });
1278
+ addCleanupResult(aggregate, page);
1279
+ return page.cursors;
1280
+ },
1281
+ state: deps.state
1282
+ });
1553
1283
  return { ...aggregate, ...cursors === void 0 ? {} : { cursors } };
1554
1284
  }
1555
1285
 
@@ -14330,250 +14060,6 @@ var Ajv = import_ajv.Ajv;
14330
14060
  // ../../node_modules/.pnpm/@modelcontextprotocol+client@2.0.0/node_modules/@modelcontextprotocol/client/dist/shimsNode.mjs
14331
14061
  var CORS_IS_POSSIBLE = false;
14332
14062
 
14333
- // ../../node_modules/.pnpm/pkce-challenge@5.0.1/node_modules/pkce-challenge/dist/index.node.js
14334
- var crypto2;
14335
- crypto2 = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
14336
- globalThis.crypto ?? // Node.js >18
14337
- import("crypto").then((m) => m.webcrypto);
14338
- async function getRandomValues(size) {
14339
- return (await crypto2).getRandomValues(new Uint8Array(size));
14340
- }
14341
- async function random(size) {
14342
- const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
14343
- const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
14344
- let result = "";
14345
- while (result.length < size) {
14346
- const randomBytes = await getRandomValues(size - result.length);
14347
- for (const randomByte of randomBytes) {
14348
- if (randomByte < evenDistCutoff) {
14349
- result += mask[randomByte % mask.length];
14350
- }
14351
- }
14352
- }
14353
- return result;
14354
- }
14355
- async function generateVerifier(length) {
14356
- return await random(length);
14357
- }
14358
- async function generateChallenge(code_verifier) {
14359
- const buffer = await (await crypto2).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
14360
- return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
14361
- }
14362
- async function pkceChallenge(length) {
14363
- if (!length)
14364
- length = 43;
14365
- if (length < 43 || length > 128) {
14366
- throw `Expected a length between 43 and 128. Received ${length}.`;
14367
- }
14368
- const verifier = await generateVerifier(length);
14369
- const challenge = await generateChallenge(verifier);
14370
- return {
14371
- code_verifier: verifier,
14372
- code_challenge: challenge
14373
- };
14374
- }
14375
-
14376
- // ../../node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/dist/index.js
14377
- var ParseError = class extends Error {
14378
- constructor(message, options) {
14379
- super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
14380
- }
14381
- };
14382
- var LF = 10;
14383
- var CR = 13;
14384
- var SPACE = 32;
14385
- function noop(_arg) {
14386
- }
14387
- function createParser(config) {
14388
- if (typeof config == "function")
14389
- throw new TypeError(
14390
- "`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?"
14391
- );
14392
- const { onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize } = config, pendingFragments = [];
14393
- let pendingFragmentsLength = 0, isFirstChunk = true, id, data = "", dataLines = 0, eventType, terminated = false;
14394
- function feed(chunk) {
14395
- if (terminated)
14396
- throw new Error(
14397
- "Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing."
14398
- );
14399
- if (isFirstChunk && (isFirstChunk = false, chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3))), pendingFragments.length === 0) {
14400
- const trailing2 = processLines(chunk);
14401
- trailing2 !== "" && (pendingFragments.push(trailing2), pendingFragmentsLength = trailing2.length), checkBufferSize();
14402
- return;
14403
- }
14404
- if (chunk.indexOf(`
14405
- `) === -1 && chunk.indexOf("\r") === -1) {
14406
- pendingFragments.push(chunk), pendingFragmentsLength += chunk.length, checkBufferSize();
14407
- return;
14408
- }
14409
- pendingFragments.push(chunk);
14410
- const input = pendingFragments.join("");
14411
- pendingFragments.length = 0, pendingFragmentsLength = 0;
14412
- const trailing = processLines(input);
14413
- trailing !== "" && (pendingFragments.push(trailing), pendingFragmentsLength = trailing.length), checkBufferSize();
14414
- }
14415
- function checkBufferSize() {
14416
- maxBufferSize !== void 0 && (pendingFragmentsLength + data.length <= maxBufferSize || (terminated = true, pendingFragments.length = 0, pendingFragmentsLength = 0, id = void 0, data = "", dataLines = 0, eventType = void 0, onError(
14417
- new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {
14418
- type: "max-buffer-size-exceeded"
14419
- })
14420
- )));
14421
- }
14422
- function processLines(chunk) {
14423
- let searchIndex = 0;
14424
- if (chunk.indexOf("\r") === -1) {
14425
- let lfIndex = chunk.indexOf(`
14426
- `, searchIndex);
14427
- for (; lfIndex !== -1; ) {
14428
- if (searchIndex === lfIndex) {
14429
- dataLines > 0 && onEvent({ id, event: eventType, data }), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
14430
- `, searchIndex);
14431
- continue;
14432
- }
14433
- const firstCharCode = chunk.charCodeAt(searchIndex);
14434
- if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
14435
- const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex);
14436
- if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
14437
- onEvent({ id, event: eventType, data: value }), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(`
14438
- `, searchIndex);
14439
- continue;
14440
- }
14441
- data = dataLines === 0 ? value : `${data}
14442
- ${value}`, dataLines++;
14443
- } else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice(
14444
- chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,
14445
- lfIndex
14446
- ) || void 0 : parseLine(chunk, searchIndex, lfIndex);
14447
- searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
14448
- `, searchIndex);
14449
- }
14450
- return chunk.slice(searchIndex);
14451
- }
14452
- for (; searchIndex < chunk.length; ) {
14453
- const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
14454
- `, searchIndex);
14455
- let lineEnd = -1;
14456
- if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1)
14457
- break;
14458
- parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++;
14459
- }
14460
- return chunk.slice(searchIndex);
14461
- }
14462
- function parseLine(chunk, start, end) {
14463
- if (start === end) {
14464
- dispatchEvent();
14465
- return;
14466
- }
14467
- const firstCharCode = chunk.charCodeAt(start);
14468
- if (isDataPrefix(chunk, start, firstCharCode)) {
14469
- const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end);
14470
- data = dataLines === 0 ? value2 : `${data}
14471
- ${value2}`, dataLines++;
14472
- return;
14473
- }
14474
- if (isEventPrefix(chunk, start, firstCharCode)) {
14475
- eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
14476
- return;
14477
- }
14478
- if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
14479
- const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
14480
- id = value2.includes("\0") ? void 0 : value2;
14481
- return;
14482
- }
14483
- if (firstCharCode === 58) {
14484
- if (onComment) {
14485
- const line2 = chunk.slice(start, end);
14486
- onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
14487
- }
14488
- return;
14489
- }
14490
- const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":");
14491
- if (fieldSeparatorIndex === -1) {
14492
- processField(line, "", line);
14493
- return;
14494
- }
14495
- const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
14496
- processField(field, value, line);
14497
- }
14498
- function processField(field, value, line) {
14499
- switch (field) {
14500
- case "event":
14501
- eventType = value || void 0;
14502
- break;
14503
- case "data":
14504
- data = dataLines === 0 ? value : `${data}
14505
- ${value}`, dataLines++;
14506
- break;
14507
- case "id":
14508
- id = value.includes("\0") ? void 0 : value;
14509
- break;
14510
- case "retry":
14511
- /^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
14512
- new ParseError(`Invalid \`retry\` value: "${value}"`, {
14513
- type: "invalid-retry",
14514
- value,
14515
- line
14516
- })
14517
- );
14518
- break;
14519
- default:
14520
- onError(
14521
- new ParseError(
14522
- `Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
14523
- { type: "unknown-field", field, value, line }
14524
- )
14525
- );
14526
- break;
14527
- }
14528
- }
14529
- function dispatchEvent() {
14530
- dataLines > 0 && onEvent({
14531
- id,
14532
- event: eventType,
14533
- data
14534
- }), id = void 0, data = "", dataLines = 0, eventType = void 0;
14535
- }
14536
- function reset(options = {}) {
14537
- if (options.consume && pendingFragments.length > 0) {
14538
- const incompleteLine = pendingFragments.join("");
14539
- parseLine(incompleteLine, 0, incompleteLine.length);
14540
- }
14541
- isFirstChunk = true, id = void 0, data = "", dataLines = 0, eventType = void 0, pendingFragments.length = 0, pendingFragmentsLength = 0, terminated = false;
14542
- }
14543
- return { feed, reset };
14544
- }
14545
- function isDataPrefix(chunk, i, firstCharCode) {
14546
- return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
14547
- }
14548
- function isEventPrefix(chunk, i, firstCharCode) {
14549
- return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
14550
- }
14551
-
14552
- // ../../node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/dist/stream.js
14553
- var EventSourceParserStream = class extends TransformStream {
14554
- constructor({ onError, onRetry, onComment, maxBufferSize } = {}) {
14555
- let parser;
14556
- super({
14557
- start(controller) {
14558
- parser = createParser({
14559
- onEvent: (event) => {
14560
- controller.enqueue(event);
14561
- },
14562
- onError(error) {
14563
- typeof onError == "function" && onError(error), (onError === "terminate" || error.type === "max-buffer-size-exceeded") && controller.error(error);
14564
- },
14565
- onRetry,
14566
- onComment,
14567
- maxBufferSize
14568
- });
14569
- },
14570
- transform(chunk) {
14571
- parser.feed(chunk);
14572
- }
14573
- });
14574
- }
14575
- };
14576
-
14577
14063
  // ../../node_modules/.pnpm/@modelcontextprotocol+client@2.0.0/node_modules/@modelcontextprotocol/client/dist/index.mjs
14578
14064
  var OAuthClientFlowError = class extends Error {
14579
14065
  static {
@@ -19459,7 +18945,7 @@ async function guardOAuth(fn) {
19459
18945
  try {
19460
18946
  return await fn();
19461
18947
  } catch (err) {
19462
- if (err instanceof TBError) throw err;
18948
+ if (isTBError(err)) throw err;
19463
18949
  if (err instanceof Error && /does not support dynamic client registration/i.test(err.message)) {
19464
18950
  throw new TBError(
19465
18951
  "invalid_argument",
@@ -19809,8 +19295,6 @@ function createPluginToolProvider(opts) {
19809
19295
 
19810
19296
  // ../app/src/providerOAuth.ts
19811
19297
  var KEY_TOKENS = "puoauth:token:";
19812
- var STATE_TTL_SEC2 = 600;
19813
- var PROVIDER_OAUTH_CALLBACK_PATH = "/~oauth/callback";
19814
19298
  async function invalidateProviderOAuth(store, nodePath) {
19815
19299
  await store.delete(KEY_TOKENS + nodePath);
19816
19300
  }
@@ -19885,15 +19369,10 @@ async function requestTokens(opts) {
19885
19369
  throw err;
19886
19370
  }
19887
19371
  }
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
19372
  async function createPkce() {
19894
- const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
19373
+ const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
19895
19374
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
19896
- return { verifier, challenge: base64url(new Uint8Array(digest)) };
19375
+ return { verifier, challenge: base64urlEncode(new Uint8Array(digest)) };
19897
19376
  }
19898
19377
  function requireProviderOAuthFetch(fetcher) {
19899
19378
  if (fetcher === void 0) {
@@ -19929,7 +19408,7 @@ async function startProviderAuthorization(opts) {
19929
19408
  {
19930
19409
  p: opts.nodePath,
19931
19410
  v: pkce?.verifier ?? "",
19932
- exp: Math.floor(opts.now.getTime() / 1e3) + STATE_TTL_SEC2
19411
+ exp: Math.floor(opts.now.getTime() / 1e3) + STATE_TTL_SEC
19933
19412
  },
19934
19413
  opts.encryptionKey
19935
19414
  );
@@ -19938,7 +19417,7 @@ async function startProviderAuthorization(opts) {
19938
19417
  authorizationUrl: buildAuthorizationUrl({
19939
19418
  config: opts.config,
19940
19419
  clientId: client.clientId,
19941
- redirectUri: opts.origin + PROVIDER_OAUTH_CALLBACK_PATH,
19420
+ redirectUri: opts.origin + OAUTH_CALLBACK_PATH,
19942
19421
  state,
19943
19422
  ...pkce === void 0 ? {} : { codeChallenge: pkce.challenge }
19944
19423
  })
@@ -19951,7 +19430,7 @@ async function finishProviderAuthorization(opts) {
19951
19430
  config: opts.config,
19952
19431
  fetcher,
19953
19432
  now: opts.now,
19954
- grant: { code: opts.code, redirectUri: opts.origin + PROVIDER_OAUTH_CALLBACK_PATH },
19433
+ grant: { code: opts.code, redirectUri: opts.origin + OAUTH_CALLBACK_PATH },
19955
19434
  ...opts.codeVerifier === "" ? {} : { codeVerifier: opts.codeVerifier },
19956
19435
  ...client
19957
19436
  });
@@ -19981,21 +19460,61 @@ async function resolveProviderAccessToken(opts) {
19981
19460
  return refreshed.accessToken;
19982
19461
  }
19983
19462
 
19463
+ // ../app/src/providers/toolCache.ts
19464
+ var KEY_PREFIX = "toolcache:";
19465
+ function toolCacheKey(path) {
19466
+ return `${KEY_PREFIX}${path}`;
19467
+ }
19468
+ function isCached(v) {
19469
+ return typeof v === "object" && v !== null && Array.isArray(v.tools) && typeof v.fetchedAt === "string";
19470
+ }
19471
+ function isFresh(fetchedAt, ttl, now) {
19472
+ const age = (Date.parse(now) - Date.parse(fetchedAt)) / 1e3;
19473
+ return Number.isFinite(age) && age >= 0 && age < ttl;
19474
+ }
19475
+ async function invalidateToolCache(store, path) {
19476
+ await store.delete(toolCacheKey(path));
19477
+ }
19478
+ function cachedTools(value) {
19479
+ return isCached(value) ? value.tools : null;
19480
+ }
19481
+ async function peekToolCache(store, path) {
19482
+ return cachedTools(await store.get(toolCacheKey(path)));
19483
+ }
19484
+ async function getTools(store, path, fetchList, opts) {
19485
+ if (!opts.refresh) {
19486
+ const raw = await store.get(toolCacheKey(path));
19487
+ if (isCached(raw) && isFresh(raw.fetchedAt, opts.ttl, opts.now)) {
19488
+ return raw.tools;
19489
+ }
19490
+ }
19491
+ await opts.beforeFresh?.();
19492
+ try {
19493
+ const tools = await fetchList();
19494
+ const record2 = { tools, fetchedAt: opts.now };
19495
+ await store.put(toolCacheKey(path), record2);
19496
+ await opts.onFresh?.(tools);
19497
+ return tools;
19498
+ } catch (error) {
19499
+ await opts.onFreshError?.();
19500
+ throw error;
19501
+ }
19502
+ }
19503
+
19984
19504
  // ../app/src/toolNodes.ts
19985
- function upstreamTools(node, provider, deps, refresh, now) {
19505
+ function upstreamTools(node, provider, deps, refresh, now, searchSync) {
19986
19506
  if (node.kind === "mcp" || node.kind === "tool") {
19987
- const sync = isMutableSearchIndex(deps.search) ? new SearchSynchronizer(deps.state, deps.search) : void 0;
19988
19507
  let marker;
19989
19508
  return getTools(deps.state, node.path, () => provider.list(), {
19990
19509
  refresh,
19991
19510
  ttl: deps.toolCacheTtlSec ?? TOOL_CACHE_TTL_DEFAULT,
19992
19511
  now,
19993
- ...sync === void 0 ? {} : {
19512
+ ...searchSync === void 0 ? {} : {
19994
19513
  beforeFresh: async () => {
19995
- marker = await sync.markNode(node.path);
19514
+ marker = await searchSync.markNode(node.path);
19996
19515
  },
19997
- onFreshError: async () => await sync.abort(marker),
19998
- onFresh: async (tools) => await sync.reconcileNodeQuietly(node.path, { marker, tools })
19516
+ onFreshError: async () => await searchSync.abort(marker),
19517
+ onFresh: async (tools) => await searchSync.reconcileNodeQuietly(node.path, { marker, tools })
19999
19518
  }
20000
19519
  });
20001
19520
  }
@@ -20130,11 +19649,11 @@ async function providerFor(node, ctx, deps) {
20130
19649
  }
20131
19650
  throw TBError.unimplemented(`kind '${node.kind}' has no tool provider`);
20132
19651
  }
20133
- async function refreshDynamicSearchNode(node, ctx, deps) {
19652
+ async function refreshDynamicSearchNode(node, ctx, deps, searchSync) {
20134
19653
  if (node.kind !== "mcp" && node.kind !== "tool" || deviceToolMarker(node) !== null) return false;
20135
19654
  try {
20136
19655
  const provider = await providerFor(node, ctx, deps);
20137
- await upstreamTools(node, provider, deps, true, (/* @__PURE__ */ new Date()).toISOString());
19656
+ await upstreamTools(node, provider, deps, true, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
20138
19657
  return true;
20139
19658
  } catch {
20140
19659
  return false;
@@ -20149,7 +19668,7 @@ async function refreshDynamicToolCache(node, ctx, deps) {
20149
19668
  now: (/* @__PURE__ */ new Date()).toISOString()
20150
19669
  });
20151
19670
  }
20152
- async function toolHelpModelFor(c, ctx, registry, path, deps) {
19671
+ async function toolHelpModelFor(c, ctx, registry, path, deps, searchSync) {
20153
19672
  let resolved;
20154
19673
  try {
20155
19674
  resolved = await registry.resolve(path);
@@ -20170,7 +19689,7 @@ async function toolHelpModelFor(c, ctx, registry, path, deps) {
20170
19689
  }
20171
19690
  const provider = await providerFor(node, ctx, deps);
20172
19691
  const refresh = c.req.query("refresh") === "1";
20173
- const raw = await upstreamTools(node, provider, deps, refresh, (/* @__PURE__ */ new Date()).toISOString());
19692
+ const raw = await upstreamTools(node, provider, deps, refresh, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
20174
19693
  const { exposed } = virtualizeTools(node.virtualize, raw);
20175
19694
  const tool = exposed.find((t) => t.name === rest);
20176
19695
  if (tool === void 0) return null;
@@ -20323,9 +19842,6 @@ async function verifyRefToken(token, secret) {
20323
19842
  function localContext(deps, node) {
20324
19843
  return deps.locals?.context?.(node.path) ?? null;
20325
19844
  }
20326
- function localCapabilities(provider) {
20327
- return contextCapabilitiesOf(provider);
20328
- }
20329
19845
  function parseS3Credentials(raw, refName) {
20330
19846
  try {
20331
19847
  const v = JSON.parse(raw);
@@ -20402,13 +19918,8 @@ async function createContextUploadGrant(node, cfg, deps, input) {
20402
19918
  uploadGrantTtlSec
20403
19919
  }, input);
20404
19920
  }
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
- };
19921
+ function objectProviderOpts(node, keyPrefix, readOnly, deps, requestUrl) {
19922
+ const opts = { nsPath: node.path, keyPrefix, readOnly };
20412
19923
  if (deps.refThresholdBytes !== void 0) opts.refThresholdBytes = deps.refThresholdBytes;
20413
19924
  if (deps.refTtlSec !== void 0) opts.presignTtlSec = deps.refTtlSec;
20414
19925
  const encKey = deps.encryptionKey;
@@ -20420,7 +19931,17 @@ async function contextProviderFor(node, cfg, deps, requestUrl) {
20420
19931
  return `${origin}/~ref/${await signRefToken({ p: node.path, k: key, exp }, encKey)}`;
20421
19932
  };
20422
19933
  }
20423
- return createObjectContextProvider(objects, opts);
19934
+ return opts;
19935
+ }
19936
+ async function contextProviderFor(node, cfg, deps, requestUrl) {
19937
+ const objects = await contextObjectStoreFor(cfg, deps);
19938
+ return createObjectContextProvider(objects, objectProviderOpts(
19939
+ node,
19940
+ contextKeyPrefix(cfg, node.path),
19941
+ cfg.readOnly ?? false,
19942
+ deps,
19943
+ requestUrl
19944
+ ));
20424
19945
  }
20425
19946
  function skillhubKeyPrefix(cfg, nodePath) {
20426
19947
  const prefix = cfg.providerConfig?.prefix;
@@ -20429,29 +19950,13 @@ function skillhubKeyPrefix(cfg, nodePath) {
20429
19950
  }
20430
19951
  async function skillhubProviderFor(node, cfg, deps, requestUrl) {
20431
19952
  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);
19953
+ return createSkillhubProvider(objects, objectProviderOpts(
19954
+ node,
19955
+ skillhubKeyPrefix(cfg, node.path),
19956
+ cfg.readOnly ?? false,
19957
+ deps,
19958
+ requestUrl
19959
+ ));
20455
19960
  }
20456
19961
  async function assertContextAlive(node, cfg, registry) {
20457
19962
  if (!isContextExpired(node.createdAt, cfg.ttl, Date.now())) return;
@@ -20522,6 +20027,257 @@ async function assertSkillhubConfig(config, deps) {
20522
20027
  }
20523
20028
  }
20524
20029
 
20030
+ // ../app/src/search/synchronizer.ts
20031
+ var DIRTY_NODE_PREFIX = "searchdirty:node:";
20032
+ var DIRTY_SUBTREE_PREFIX = "searchdirty:subtree:";
20033
+ function isMutableSearchIndex(value) {
20034
+ if (typeof value !== "object" || value === null) return false;
20035
+ const index = value;
20036
+ return typeof index.replace === "function" && typeof index.remove === "function" && typeof index.removePrefix === "function" && typeof index.rebuild === "function" && typeof index.initialized === "function";
20037
+ }
20038
+ function toolNode(node) {
20039
+ return (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config?.kind === node.kind;
20040
+ }
20041
+ function httpTools(node) {
20042
+ if (node.config?.kind !== "http") return null;
20043
+ return node.config.tools.map((tool) => {
20044
+ const spec = {
20045
+ description: tool.description,
20046
+ effect: effectFor(tool),
20047
+ name: tool.name
20048
+ };
20049
+ if (tool.inputSchema !== void 0) spec.inputSchema = tool.inputSchema;
20050
+ return spec;
20051
+ });
20052
+ }
20053
+ function deviceTools(node) {
20054
+ if (node.config?.kind !== "tool") return null;
20055
+ const cmds = node.config.providerConfig?.cmds;
20056
+ if (!Array.isArray(cmds)) return null;
20057
+ return cmds;
20058
+ }
20059
+ async function canonicalSearchTools(state, nodes) {
20060
+ const unique = new Map(nodes.map((node) => [node.path, node]));
20061
+ const dynamicPaths = [...unique.values()].filter((node) => node.kind !== "http" && deviceTools(node) === null).map((node) => node.path);
20062
+ const cached = dynamicPaths.length === 0 ? /* @__PURE__ */ new Map() : await state.getMany(dynamicPaths.map(toolCacheKey));
20063
+ const result = /* @__PURE__ */ new Map();
20064
+ for (const node of unique.values()) {
20065
+ const tools = node.kind === "http" ? httpTools(node) : deviceTools(node) ?? cachedTools(cached.get(toolCacheKey(node.path)));
20066
+ if (tools !== null) result.set(node.path, tools);
20067
+ }
20068
+ return result;
20069
+ }
20070
+ function documentsFor(path, tools, feedback) {
20071
+ return tools.map((tool) => ({ path, tool, ...feedback === "" ? {} : { feedback } }));
20072
+ }
20073
+ var SearchSynchronizer = class {
20074
+ constructor(state, search) {
20075
+ this.state = state;
20076
+ this.search = search;
20077
+ this.registry = new NodeRegistryStore(state);
20078
+ }
20079
+ state;
20080
+ search;
20081
+ registry;
20082
+ seedPromise;
20083
+ async mark(path, kind) {
20084
+ const canonical = normalizeToolSearchPath(path);
20085
+ const marker = {
20086
+ createdAt: Date.now(),
20087
+ // Marker 只是审计提示,正确性由每次 canonical audit 保证。每类固定一个 key,
20088
+ // 避免 overflow/device hello 用 UUID marker 把 KV 与扫描成本无界放大。
20089
+ key: `${kind === "node" ? DIRTY_NODE_PREFIX : DIRTY_SUBTREE_PREFIX}pending`,
20090
+ kind,
20091
+ path: canonical
20092
+ };
20093
+ await this.state.put(marker.key, marker);
20094
+ return marker;
20095
+ }
20096
+ async markNode(path) {
20097
+ return await this.mark(path, "node");
20098
+ }
20099
+ async markSubtree(path) {
20100
+ return await this.mark(path, "subtree");
20101
+ }
20102
+ async abort(marker) {
20103
+ if (marker !== void 0 && marker.expectedDigest === void 0) {
20104
+ await this.state.delete(marker.key);
20105
+ }
20106
+ }
20107
+ async dirtyMarkers() {
20108
+ const markers = [];
20109
+ for (const prefix of [DIRTY_NODE_PREFIX, DIRTY_SUBTREE_PREFIX]) {
20110
+ let cursor;
20111
+ do {
20112
+ const page = await this.state.list(prefix, {
20113
+ limit: 200,
20114
+ ...cursor === void 0 ? {} : { cursor }
20115
+ });
20116
+ for (const item of page.items) {
20117
+ const marker = item.value;
20118
+ if (typeof marker.createdAt === "number" && marker.key === item.key && (marker.kind === "node" || marker.kind === "subtree") && typeof marker.path === "string") {
20119
+ markers.push(marker);
20120
+ }
20121
+ }
20122
+ cursor = page.cursor;
20123
+ } while (cursor !== void 0);
20124
+ }
20125
+ return markers;
20126
+ }
20127
+ async commitMarker(marker, expectedDigest) {
20128
+ if (marker === void 0) return;
20129
+ const committed = {
20130
+ ...marker,
20131
+ committedAt: Date.now(),
20132
+ expectedDigest
20133
+ };
20134
+ await this.state.put(marker.key, committed);
20135
+ Object.assign(marker, committed);
20136
+ }
20137
+ async finishMarker(marker) {
20138
+ if (marker !== void 0) await this.state.delete(marker.key);
20139
+ }
20140
+ async feedbackText(path, snapshot) {
20141
+ if (snapshot !== void 0) return selectFeedbackSearchText(snapshot);
20142
+ return selectFeedbackSearchText(parseFeedbackEntries(await this.state.get(KEY_FEEDBACK + path)));
20143
+ }
20144
+ async rawTools(node, override) {
20145
+ if (override !== void 0) return [...override];
20146
+ if (node.kind === "http") return httpTools(node);
20147
+ const device = deviceTools(node);
20148
+ if (device !== null) return device;
20149
+ if (node.kind === "mcp" || node.kind === "tool") {
20150
+ return await peekToolCache(this.state, node.path);
20151
+ }
20152
+ return null;
20153
+ }
20154
+ indexableRecords(path, tools, feedback) {
20155
+ try {
20156
+ return serializeToolSearchSnapshot(path, tools, feedback);
20157
+ } catch (error) {
20158
+ if (isTBError(error)) return null;
20159
+ throw error;
20160
+ }
20161
+ }
20162
+ async reconcileNode(path, opts = {}) {
20163
+ const canonical = normalizeToolSearchPath(path);
20164
+ const node = await this.registry.get(canonical).catch(() => null);
20165
+ if (node === null || !toolNode(node)) {
20166
+ await this.commitMarker(opts.marker, null);
20167
+ await this.search.remove(canonical);
20168
+ await this.finishMarker(opts.marker);
20169
+ return;
20170
+ }
20171
+ const tools = await this.rawTools(node, opts.tools);
20172
+ if (tools === null) {
20173
+ await this.search.remove(canonical);
20174
+ await this.finishMarker(opts.marker);
20175
+ return;
20176
+ }
20177
+ if (tools.length === 0) {
20178
+ await this.commitMarker(opts.marker, null);
20179
+ await this.search.remove(canonical);
20180
+ await this.finishMarker(opts.marker);
20181
+ return;
20182
+ }
20183
+ const root = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
20184
+ if (root.truncated) {
20185
+ await this.finishMarker(opts.marker);
20186
+ return;
20187
+ }
20188
+ const feedback = await this.feedbackText(canonical, opts.feedback);
20189
+ const records = this.indexableRecords(canonical, tools, feedback);
20190
+ if (records === null) {
20191
+ await this.commitMarker(opts.marker, null);
20192
+ await this.search.remove(canonical);
20193
+ await this.finishMarker(opts.marker);
20194
+ return;
20195
+ }
20196
+ const digest = records.length === 0 ? null : toolSearchSnapshotDigest(records);
20197
+ await this.commitMarker(opts.marker, digest);
20198
+ await this.search.replace(canonical, tools, {
20199
+ feedback
20200
+ });
20201
+ await this.finishMarker(opts.marker);
20202
+ }
20203
+ /** mutation 热路径使用:canonical 已成功时,派生同步失败只保留 marker。 */
20204
+ async reconcileNodeQuietly(path, opts = {}) {
20205
+ try {
20206
+ await this.reconcileNode(path, opts);
20207
+ } catch {
20208
+ }
20209
+ }
20210
+ async rebuildAll(markers = [], opts = {}) {
20211
+ const snapshot = opts.canonicalNodes === void 0 ? await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT) : { items: [...opts.canonicalNodes], truncated: false };
20212
+ if (snapshot.truncated) return false;
20213
+ const nodes = snapshot.items.filter(toolNode);
20214
+ const authoritativeEmpty = new Set(opts.authoritativeEmpty ?? []);
20215
+ const keys = nodes.flatMap((node) => [KEY_FEEDBACK + node.path, toolCacheKey(node.path)]);
20216
+ const values = await this.state.getMany(keys);
20217
+ const documents = [];
20218
+ for (const node of nodes) {
20219
+ if (authoritativeEmpty.has(node.path)) continue;
20220
+ let tools;
20221
+ if (node.kind === "http") {
20222
+ tools = httpTools(node);
20223
+ } else {
20224
+ tools = deviceTools(node) ?? cachedTools(values.get(toolCacheKey(node.path)));
20225
+ }
20226
+ if (tools === null) continue;
20227
+ const feedback = selectFeedbackSearchText(
20228
+ parseFeedbackEntries(values.get(KEY_FEEDBACK + node.path))
20229
+ );
20230
+ if (this.indexableRecords(node.path, tools, feedback) === null) continue;
20231
+ documents.push(...documentsFor(node.path, tools, feedback));
20232
+ }
20233
+ await this.search.rebuild(documents);
20234
+ await Promise.all(markers.map(async (marker) => {
20235
+ await this.state.delete(marker.key).catch(() => {
20236
+ });
20237
+ }));
20238
+ return true;
20239
+ }
20240
+ /** 每次搜索前审计 canonical 快照;同快照 rebuild 不 bump revision。 */
20241
+ async ensureReady() {
20242
+ const snapshot = await this.registry.rootSnapshot(TOOL_SEARCH_AUDIT_NODE_LIMIT);
20243
+ if (snapshot.truncated) {
20244
+ if (await this.search.initialized()) return;
20245
+ throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
20246
+ }
20247
+ const dirty = await this.dirtyMarkers();
20248
+ await this.rebuildAll(dirty, { canonicalNodes: snapshot.items });
20249
+ }
20250
+ /** 首次 registry/device mutation 前建立 last-known-good,避免并发溢出抢在首次 seed 前。 */
20251
+ async ensureSeeded() {
20252
+ if (await this.search.initialized()) return;
20253
+ if (this.seedPromise === void 0) {
20254
+ this.seedPromise = (async () => {
20255
+ if (!await this.rebuildAll()) {
20256
+ throw new TBError("rate_limited", "registry \u8D85\u51FA\u5DE5\u5177\u641C\u7D22 canonical audit \u5BB9\u91CF");
20257
+ }
20258
+ })();
20259
+ }
20260
+ try {
20261
+ await this.seedPromise;
20262
+ } finally {
20263
+ this.seedPromise = void 0;
20264
+ }
20265
+ }
20266
+ async removeSubtreeQuietly(path, marker) {
20267
+ try {
20268
+ const canonical = normalizeToolSearchPath(path);
20269
+ if ((await this.registry.subtree(canonical)).length === 0) {
20270
+ await this.commitMarker(marker, null);
20271
+ await this.search.removePrefix(canonical);
20272
+ await this.finishMarker(marker);
20273
+ } else {
20274
+ await this.rebuildAll(marker === void 0 ? [] : [marker]);
20275
+ }
20276
+ } catch {
20277
+ }
20278
+ }
20279
+ };
20280
+
20525
20281
  // ../app/src/deviceHello.ts
20526
20282
  function assertDeviceId(deviceId) {
20527
20283
  if (!/^[A-Za-z0-9._-]+$/.test(deviceId)) {
@@ -20561,6 +20317,7 @@ function customNodeInput(mountPath, deviceId, raw) {
20561
20317
  return input;
20562
20318
  }
20563
20319
  function nodesForHello(mountPath, deviceId, expose) {
20320
+ const rawTargets = [mountPath];
20564
20321
  const nodes = [
20565
20322
  {
20566
20323
  path: mountPath,
@@ -20569,6 +20326,7 @@ function nodesForHello(mountPath, deviceId, expose) {
20569
20326
  }
20570
20327
  ];
20571
20328
  if (expose.shell !== void 0) {
20329
+ rawTargets.push(joinTreePath(mountPath, "shell"));
20572
20330
  nodes.push({
20573
20331
  path: joinTreePath(mountPath, "shell"),
20574
20332
  kind: "device",
@@ -20577,6 +20335,7 @@ function nodesForHello(mountPath, deviceId, expose) {
20577
20335
  });
20578
20336
  }
20579
20337
  if (expose.fs !== void 0) {
20338
+ rawTargets.push(joinTreePath(mountPath, "fs"));
20580
20339
  nodes.push({
20581
20340
  path: joinTreePath(mountPath, "fs"),
20582
20341
  kind: "context",
@@ -20590,11 +20349,13 @@ function nodesForHello(mountPath, deviceId, expose) {
20590
20349
  });
20591
20350
  }
20592
20351
  for (const raw of expose.nodes ?? []) {
20352
+ rawTargets.push(joinTreePath(mountPath, raw.path));
20593
20353
  nodes.push(customNodeInput(mountPath, deviceId, raw));
20594
20354
  }
20355
+ assertNoCollision(rawTargets, `\u8BBE\u5907 '${deviceId}' \u7684 expose \u8DEF\u5F84`);
20595
20356
  return nodes;
20596
20357
  }
20597
- async function assertRegisterPath(registry, ctx, targetPath) {
20358
+ async function assertRegisterPath(registry, ctx, targetPath, reservedRoots) {
20598
20359
  if (!check(ctx, targetPath, "register").allow) {
20599
20360
  throw new TBError("permission_denied", `no scope grants 'register' on '${targetPath}'`);
20600
20361
  }
@@ -20612,7 +20373,8 @@ async function assertRegisterPath(registry, ctx, targetPath) {
20612
20373
  },
20613
20374
  targetPath,
20614
20375
  action: "write",
20615
- existing
20376
+ existing,
20377
+ ...reservedRoots !== void 0 ? { reservedRoots } : {}
20616
20378
  });
20617
20379
  if (!res.allow) throw res.error;
20618
20380
  }
@@ -20633,7 +20395,7 @@ async function processDeviceHello(opts) {
20633
20395
  const inputs = nodesForHello(mountPath, hello.deviceId, hello.expose);
20634
20396
  const registry = new NodeRegistryStore(store);
20635
20397
  for (const input of inputs) {
20636
- await assertRegisterPath(registry, authCtx, input.path);
20398
+ await assertRegisterPath(registry, authCtx, input.path, opts.reservedRoots);
20637
20399
  assertSecretRefUse(authCtx.scopes, input.config);
20638
20400
  }
20639
20401
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
@@ -20662,15 +20424,43 @@ async function processDeviceHello(opts) {
20662
20424
  return { mountPath, keyId: authCtx.keyId, searchIndexed };
20663
20425
  }
20664
20426
 
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`);
20427
+ // ../app/src/deviceLifecycle.ts
20428
+ async function reverifyDeviceAuthority(opts) {
20429
+ const authCtx = await identify(opts.store, opts.authorization, (/* @__PURE__ */ new Date()).toISOString());
20430
+ if (authCtx === null || authCtx.keyId !== opts.keyId) return false;
20431
+ return checkRegisterPath({
20432
+ sk: {
20433
+ scopes: authCtx.scopes,
20434
+ id: authCtx.keyId,
20435
+ ...authCtx.registerPaths !== void 0 ? { registerPaths: authCtx.registerPaths } : {}
20436
+ },
20437
+ targetPath: opts.mountPath,
20438
+ action: "write",
20439
+ existing: null,
20440
+ ...opts.reservedRoots !== void 0 ? { reservedRoots: opts.reservedRoots } : {}
20441
+ }).allow;
20442
+ }
20443
+ async function markDeviceDisconnected(opts) {
20444
+ try {
20445
+ await opts.registry.setOnline(opts.mountPath, false, opts.now);
20446
+ } catch {
20671
20447
  }
20672
- return actual;
20673
20448
  }
20449
+ async function reclaimDeviceSubtree(opts) {
20450
+ const searchSync = opts.search === void 0 ? void 0 : new SearchSynchronizer(opts.state, opts.search);
20451
+ const marker = await searchSync?.markSubtree(opts.mountPath);
20452
+ try {
20453
+ await opts.registry.deleteSubtree(opts.mountPath);
20454
+ } catch {
20455
+ }
20456
+ await searchSync?.removeSubtreeQuietly(opts.mountPath, marker);
20457
+ }
20458
+ function deviceSearchCapacityWarning(deviceId, mountPath) {
20459
+ 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.`;
20460
+ }
20461
+
20462
+ // ../app/src/deviceMailbox.ts
20463
+ var KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS = "sys:device-mailbox-cleanup-progress:v1";
20674
20464
  function parseProgress(value) {
20675
20465
  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
20466
  return value;
@@ -20684,39 +20474,27 @@ async function cleanupDeviceMailbox(deps, opts = {}) {
20684
20474
  throw new TBError("unavailable", "device mailbox cleanup requires StateStore.compareAndSwap");
20685
20475
  }
20686
20476
  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;
20477
+ const maxPages = Math.min(positiveInt(opts.maxPages, 8, "maxPages"), 64);
20478
+ const limit = Math.min(positiveInt(opts.limit, 200, "limit"), 200);
20692
20479
  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
- }
20480
+ const cursor = await runCursorCleanup({
20481
+ // {cursor,revision} 即持久形状,原样落库。
20482
+ encodeProgress: (progress) => progress,
20483
+ maxPages,
20484
+ parseProgress,
20485
+ progressKey: KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
20486
+ runPage: async (pageCursor) => {
20487
+ const page = await mailbox.cleanup({
20488
+ limit,
20489
+ ...pageCursor === void 0 ? {} : { cursor: pageCursor }
20490
+ });
20491
+ aggregate.deleted += page.deleted;
20492
+ aggregate.expired += page.expired;
20493
+ aggregate.scanned += page.scanned;
20494
+ return page.cursor;
20495
+ },
20496
+ state: deps.state
20497
+ });
20720
20498
  return { ...aggregate, ...cursor === void 0 ? {} : { cursor } };
20721
20499
  }
20722
20500
 
@@ -36622,7 +36400,7 @@ async function readRemoteResponseBody(response, maxBytes, signal) {
36622
36400
  chunks.push(result.value);
36623
36401
  }
36624
36402
  } catch (error) {
36625
- if (error instanceof TBError) throw error;
36403
+ if (isTBError(error)) throw error;
36626
36404
  if (isAbortError(error, signal)) throw remoteAbortError();
36627
36405
  throw normalizeUpstreamError({
36628
36406
  kind: "network",
@@ -37320,6 +37098,99 @@ async function handleFeedbackDelete(c, env) {
37320
37098
  return feedbackJson(feedbackRemoveResponseSchema, { ok: true });
37321
37099
  }
37322
37100
 
37101
+ // ../app/src/responses.ts
37102
+ function tbErrorResponse(err) {
37103
+ return new Response(JSON.stringify(tbErrorBodySchema.parse(err.toJSON())), {
37104
+ status: err.httpStatus,
37105
+ headers: { "content-type": "application/json; charset=utf-8" }
37106
+ });
37107
+ }
37108
+ function withSecurityHeaders(res) {
37109
+ if (res.status === 101 || res.webSocket != null) return res;
37110
+ const apply = (headers) => {
37111
+ if (!headers.has("content-security-policy")) {
37112
+ headers.set(
37113
+ "content-security-policy",
37114
+ "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'"
37115
+ );
37116
+ }
37117
+ headers.set("x-content-type-options", "nosniff");
37118
+ headers.set("x-frame-options", "DENY");
37119
+ headers.set("referrer-policy", "no-referrer");
37120
+ };
37121
+ try {
37122
+ apply(res.headers);
37123
+ return res;
37124
+ } catch {
37125
+ const headers = new Headers(res.headers);
37126
+ apply(headers);
37127
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
37128
+ }
37129
+ }
37130
+ async function runHandler(fn) {
37131
+ try {
37132
+ return await fn();
37133
+ } catch (err) {
37134
+ if (isTBError(err)) return tbErrorResponse(err);
37135
+ return tbErrorResponse(new TBError("internal", "internal error"));
37136
+ }
37137
+ }
37138
+ function renderHelp(model, rep) {
37139
+ if (rep === "json") {
37140
+ return new Response(JSON.stringify(helpJsonSchema.parse(renderHelpJson(model))), {
37141
+ headers: { "content-type": contentTypeFor("json") }
37142
+ });
37143
+ }
37144
+ if (rep === "markdown") {
37145
+ return new Response(renderHelpMarkdown(model), {
37146
+ headers: { "content-type": contentTypeFor("markdown") }
37147
+ });
37148
+ }
37149
+ return new Response(renderHelpDsl(model), {
37150
+ headers: { "content-type": contentTypeFor("dsl") }
37151
+ });
37152
+ }
37153
+ async function enrichHelp(model, path, store) {
37154
+ try {
37155
+ const [annotation, feedback] = await Promise.all([
37156
+ new AnnotationStore(store).get(path),
37157
+ path === "" ? Promise.resolve([]) : new FeedbackStore(store).helpItems(path)
37158
+ ]);
37159
+ return {
37160
+ ...model,
37161
+ ...annotation !== null ? { note: annotation.text } : {},
37162
+ ...feedback.length > 0 ? { feedback } : {}
37163
+ };
37164
+ } catch {
37165
+ return model;
37166
+ }
37167
+ }
37168
+ function renderResult(value, rep) {
37169
+ const json = JSON.stringify(value ?? null);
37170
+ if (rep === "json") {
37171
+ return new Response(json, { headers: { "content-type": contentTypeFor("json") } });
37172
+ }
37173
+ return new Response(`\`\`\`json
37174
+ ${json}
37175
+ \`\`\`
37176
+ `, {
37177
+ headers: { "content-type": contentTypeFor("markdown") }
37178
+ });
37179
+ }
37180
+ function renderTreeDsl(tree) {
37181
+ const lines = [];
37182
+ const walk = (n, depth) => {
37183
+ const indent = " ".repeat(depth);
37184
+ const label = n.path === "" ? "/" : n.path;
37185
+ const trunc = n.truncated ? " \u2026" : "";
37186
+ lines.push(`${indent}${label} [${n.kind}] ${n.description}${trunc}`);
37187
+ for (const child of n.children ?? []) walk(child, depth + 1);
37188
+ };
37189
+ walk(tree, 0);
37190
+ return `${lines.join("\n")}
37191
+ `;
37192
+ }
37193
+
37323
37194
  // ../app/src/routes/deviceMailbox.ts
37324
37195
  var DEVICE_MAILBOX_IDEMPOTENCY_HEADER = "x-tb-idempotency-key";
37325
37196
  var DEVICE_MAILBOX_HTTP_BODY_BYTES = 272 * 1024;
@@ -37356,7 +37227,7 @@ async function mailboxJsonObject(c, opts = {}) {
37356
37227
  }
37357
37228
  raw += decoder.decode();
37358
37229
  } catch (cause) {
37359
- if (cause instanceof TBError) throw cause;
37230
+ if (isTBError(cause)) throw cause;
37360
37231
  throw invalidRequest("body must be valid UTF-8 JSON");
37361
37232
  }
37362
37233
  }
@@ -37395,7 +37266,7 @@ function canManage(ctx, operation) {
37395
37266
  function assertCanManage(ctx, operation) {
37396
37267
  if (!canManage(ctx, operation)) throw TBError.notFound("device operation not found");
37397
37268
  }
37398
- async function assertCurrentDeviceCredential(c, target) {
37269
+ async function assertCurrentDeviceCredential(c, target, reservedRoots) {
37399
37270
  const ctx = c.get("ctx");
37400
37271
  if (ctx.keyId !== target.deviceKeyId) {
37401
37272
  throw new TBError("permission_denied", "device credential does not own this operation");
@@ -37414,7 +37285,8 @@ async function assertCurrentDeviceCredential(c, target) {
37414
37285
  scopes: ctx.scopes,
37415
37286
  ...ctx.registerPaths === void 0 ? {} : { registerPaths: ctx.registerPaths }
37416
37287
  },
37417
- targetPath: target.mountPath
37288
+ targetPath: target.mountPath,
37289
+ ...reservedRoots === void 0 ? {} : { reservedRoots }
37418
37290
  });
37419
37291
  if (!allowed.allow) throw allowed.error;
37420
37292
  }
@@ -37438,212 +37310,118 @@ async function enqueueDeviceCommand(c, env, input) {
37438
37310
  response.headers.set("x-tb-delivery", "mailbox");
37439
37311
  return response;
37440
37312
  }
37441
- async function handleDeviceMailboxControl(c, env) {
37442
- const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
37443
- const body = await mailboxJsonObject(c);
37313
+ async function getOperation(c, env) {
37314
+ const input = parseBody(deviceOperationIdentityRequestSchema, await mailboxJsonObject(c));
37315
+ const result = await env.mailbox().get(input.deviceId, input.operationId);
37316
+ assertCanManage(c.get("ctx"), result);
37317
+ return c.json(deviceOperationDetailSchema.parse(result));
37318
+ }
37319
+ async function listOperations(c, env) {
37320
+ const input = parseBody(deviceOperationListRequestSchema, await mailboxJsonObject(c));
37444
37321
  const mailbox = env.mailbox();
37445
37322
  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) {
37323
+ const limit = Math.min(
37324
+ input.opts?.limit ?? DEVICE_MAILBOX_LIST_LIMIT_DEFAULT,
37325
+ DEVICE_MAILBOX_LIST_LIMIT_MAX
37326
+ );
37327
+ let cursor = input.opts?.cursor;
37328
+ const items = [];
37329
+ while (items.length < limit) {
37330
+ const previous = cursor;
37331
+ const page = await mailbox.list({
37332
+ deviceId: input.deviceId,
37333
+ limit: limit - items.length,
37334
+ ...cursor === void 0 ? {} : { cursor },
37335
+ ...input.opts?.states === void 0 ? {} : { states: input.opts.states }
37336
+ });
37337
+ items.push(...page.items.filter((item) => canManage(ctx, item)));
37338
+ cursor = page.cursor;
37339
+ if (cursor === void 0) {
37479
37340
  return c.json(deviceOperationListResponseSchema.parse({ items }));
37480
37341
  }
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
- }
37342
+ if (cursor === previous) {
37343
+ throw new TBError("internal", "device mailbox list cursor did not advance");
37497
37344
  }
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
37345
  }
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
- })));
37346
+ const resumeCursor = cursor;
37347
+ if (resumeCursor === void 0) {
37348
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37523
37349
  }
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({
37350
+ let probeCursor = resumeCursor;
37351
+ while (probeCursor !== void 0) {
37352
+ const previous = probeCursor;
37353
+ const page = await mailbox.list({
37546
37354
  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
- );
37355
+ cursor: probeCursor,
37356
+ limit: DEVICE_MAILBOX_LIST_LIMIT_MAX,
37357
+ ...input.opts?.states === void 0 ? {} : { states: input.opts.states }
37358
+ });
37359
+ if (page.items.some((item) => canManage(ctx, item))) {
37360
+ return c.json(deviceOperationListResponseSchema.parse({ items, cursor: resumeCursor }));
37361
+ }
37362
+ probeCursor = page.cursor;
37363
+ if (probeCursor === previous) {
37364
+ throw new TBError("internal", "device mailbox list cursor did not advance");
37571
37365
  }
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
37366
  }
37367
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37584
37368
  }
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
- }
37369
+ async function cancelOperation(c, env) {
37370
+ const input = parseBody(deviceOperationIdentityRequestSchema, await mailboxJsonObject(c));
37371
+ const mailbox = env.mailbox();
37372
+ const current = await mailbox.get(input.deviceId, input.operationId);
37373
+ assertCanManage(c.get("ctx"), current);
37374
+ return c.json(deviceOperationDetailSchema.parse(
37375
+ await mailbox.cancel(input.deviceId, input.operationId)
37376
+ ));
37377
+ }
37378
+ function deviceAuthorize(c, env) {
37379
+ return async (target) => await assertCurrentDeviceCredential(c, target, env.deps.reservedRoots);
37380
+ }
37381
+ async function claimOperations(c, env) {
37382
+ const input = parseBody(deviceOperationClaimRequestSchema, await mailboxJsonObject(c));
37383
+ return c.json(deviceOperationClaimResponseSchema.parse(await env.mailbox().claim({
37384
+ ...input,
37385
+ deviceKeyId: c.get("ctx").keyId,
37386
+ authorize: deviceAuthorize(c, env)
37387
+ })));
37592
37388
  }
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
- });
37389
+ async function renewLeases(c, env) {
37390
+ const input = parseBody(deviceOperationRenewRequestSchema, await mailboxJsonObject(c));
37391
+ return c.json(deviceOperationRenewResponseSchema.parse(await env.mailbox().renew({
37392
+ ...input,
37393
+ deviceKeyId: c.get("ctx").keyId,
37394
+ authorize: deviceAuthorize(c, env)
37395
+ })));
37607
37396
  }
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 } : {}
37397
+ async function completeOperation(c, env) {
37398
+ const input = parseBody(deviceOperationCompleteRequestSchema, await mailboxJsonObject(c));
37399
+ let completion;
37400
+ if (input.outcome === "succeeded") {
37401
+ completion = { outcome: "succeeded", result: input.result };
37402
+ } else if (input.outcome === "result_unknown") {
37403
+ completion = {
37404
+ outcome: "result_unknown",
37405
+ ...input.error === void 0 ? {} : { error: input.error }
37618
37406
  };
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
- `;
37407
+ } else {
37408
+ completion = { outcome: input.outcome, error: input.error };
37409
+ }
37410
+ return c.json(deviceOperationDetailSchema.parse(await env.mailbox().complete({
37411
+ deviceId: input.deviceId,
37412
+ operationId: input.operationId,
37413
+ leaseId: input.leaseId,
37414
+ deviceKeyId: c.get("ctx").keyId,
37415
+ authorize: deviceAuthorize(c, env)
37416
+ }, completion)));
37417
+ }
37418
+ function registerDeviceMailboxRoutes(app, env) {
37419
+ app.post("/~device/operations/get", (c) => runHandler(async () => await getOperation(c, env)));
37420
+ app.post("/~device/operations/list", (c) => runHandler(async () => await listOperations(c, env)));
37421
+ app.post("/~device/operations/cancel", (c) => runHandler(async () => await cancelOperation(c, env)));
37422
+ app.post("/~device/mailbox/claim", (c) => runHandler(async () => await claimOperations(c, env)));
37423
+ app.post("/~device/mailbox/renew", (c) => runHandler(async () => await renewLeases(c, env)));
37424
+ app.post("/~device/mailbox/complete", (c) => runHandler(async () => await completeOperation(c, env)));
37647
37425
  }
37648
37426
 
37649
37427
  // ../app/src/federation.ts
@@ -37867,6 +37645,24 @@ function assertRemoteConfigAllowed(config, settings) {
37867
37645
  assertRemoteAllowed(baseUrl, settings);
37868
37646
  }
37869
37647
 
37648
+ // ../app/src/registryMutation.ts
37649
+ async function assertNodeConfigMutation(opts) {
37650
+ const { action, config, ctx, deps, registry, targetPath } = opts;
37651
+ assertRemoteConfigAllowed(config, await resolveRemoteSettings(deps.state, deps.remote));
37652
+ await assertRegisterPath2(registry, ctx, targetPath, action, deps);
37653
+ assertSecretRefUse(ctx.scopes, config);
37654
+ await assertMcpOAuthConfig(config, deps.secrets);
37655
+ await assertContextConfig(config, deps);
37656
+ await assertSkillhubConfig(config, deps);
37657
+ await assertToolConfig(config, deps, ctx, targetPath);
37658
+ }
37659
+ async function invalidateNodeDerivedState(store, path) {
37660
+ await invalidateToolCache(store, path);
37661
+ await invalidateMcpEra(store, path);
37662
+ await invalidateMcpOAuth(store, path);
37663
+ await invalidateProviderOAuth(store, path);
37664
+ }
37665
+
37870
37666
  // ../app/src/routes/register.ts
37871
37667
  async function authorizeToolNode(c, env, node, encryptionKey) {
37872
37668
  const { deps } = env;
@@ -37923,16 +37719,17 @@ async function handleRegister(c, env) {
37923
37719
  );
37924
37720
  }
37925
37721
  const body = parseNodeInput(raw);
37926
- assertRemoteConfigAllowed(body.config, await resolveRemoteSettings(store, deps.remote));
37927
37722
  if (!check(ctx, path, "register").allow) {
37928
37723
  throw new TBError("permission_denied", `no scope grants 'register' on '${path}'`);
37929
37724
  }
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);
37725
+ await assertNodeConfigMutation({
37726
+ action: "write",
37727
+ config: body.config,
37728
+ ctx,
37729
+ deps,
37730
+ registry,
37731
+ targetPath: body.path
37732
+ });
37936
37733
  await searchSync?.ensureSeeded();
37937
37734
  const now = (/* @__PURE__ */ new Date()).toISOString();
37938
37735
  const marker = await searchSync?.markNode(body.path);
@@ -37943,12 +37740,9 @@ async function handleRegister(c, env) {
37943
37740
  await searchSync?.abort(marker);
37944
37741
  throw error;
37945
37742
  }
37946
- await invalidateToolCache(store, body.path);
37947
- await invalidateMcpEra(store, body.path);
37948
- await invalidateMcpOAuth(store, body.path);
37949
- await invalidateProviderOAuth(store, body.path);
37743
+ await invalidateNodeDerivedState(store, body.path);
37950
37744
  await searchSync?.reconcileNodeQuietly(body.path, { marker });
37951
- if (await refreshDynamicSearchNode(node, ctx, deps)) await searchSync?.abort(marker);
37745
+ if (await refreshDynamicSearchNode(node, ctx, deps, searchSync)) await searchSync?.abort(marker);
37952
37746
  return new Response(JSON.stringify(registryNodeSchema.parse(node)), {
37953
37747
  headers: { "content-type": contentTypeFor("json") }
37954
37748
  });
@@ -38045,7 +37839,7 @@ async function handleDescribe(c, env) {
38045
37839
  await assertContextAlive(node, node.config, registry);
38046
37840
  const cfg = node.config;
38047
37841
  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 ?? [];
37842
+ 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
37843
  if ((cfg.provider === "r2" || cfg.provider === "s3") && cfg.readOnly !== true && await contextDirectUploadAvailable(cfg, deps)) {
38050
37844
  capabilities = [...capabilities, CONTEXT_DIRECT_UPLOAD_CAPABILITY];
38051
37845
  }
@@ -38497,7 +38291,7 @@ function parseRecord(value) {
38497
38291
  topologyDigest: value.topologyDigest
38498
38292
  });
38499
38293
  } catch (error) {
38500
- if (error instanceof TBError) throw error;
38294
+ if (isTBError(error)) throw error;
38501
38295
  throw corruptSession();
38502
38296
  }
38503
38297
  }
@@ -38713,12 +38507,8 @@ var FederatedSearchSessionStore = class {
38713
38507
  }
38714
38508
  };
38715
38509
 
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;
38510
+ // ../app/src/search/compactTool.ts
38511
+ function compactSearchTool(tool) {
38722
38512
  const compact = { ...tool };
38723
38513
  delete compact.inputSchema;
38724
38514
  delete compact.outputSchema;
@@ -38738,6 +38528,14 @@ function projectSearchTool(tool, detail) {
38738
38528
  }
38739
38529
  return compact;
38740
38530
  }
38531
+
38532
+ // ../app/src/search/localSearch.ts
38533
+ function normalizedEffect(effect) {
38534
+ return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
38535
+ }
38536
+ function projectSearchTool(tool, detail) {
38537
+ return detail === "full" ? tool : compactSearchTool(tool);
38538
+ }
38741
38539
  async function executeLocalSearch(deps, request) {
38742
38540
  const { ctx, search, searchSync, state } = deps;
38743
38541
  const query = request.query;
@@ -38867,7 +38665,7 @@ function isPrefix(prefix, path) {
38867
38665
  return prefix === "" || prefix === path || path.startsWith(`${prefix}/`);
38868
38666
  }
38869
38667
  function controlledStatus(error) {
38870
- if (error instanceof TBError) {
38668
+ if (isTBError(error)) {
38871
38669
  if (error.message.includes("\u73AF")) return "cycle";
38872
38670
  if (error.message.includes("\u8DF3\u6570")) return "hop_limit";
38873
38671
  if (error.message.includes("aborted")) return "timed_out";
@@ -38886,31 +38684,14 @@ function invalidContinuation() {
38886
38684
  });
38887
38685
  }
38888
38686
  function continuationSourceError(error) {
38889
- if (error instanceof TBError) {
38687
+ if (isTBError(error)) {
38890
38688
  if (error.code === "invalid_argument") return error;
38891
38689
  if (error.message.includes("source work budget exhausted")) return invalidContinuation();
38892
38690
  }
38893
38691
  return retryableContinuationError();
38894
38692
  }
38895
38693
  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 };
38694
+ return { ...item, tool: compactSearchTool(item.tool) };
38914
38695
  }
38915
38696
  function normalizedEffect2(effect) {
38916
38697
  return effect === "read" || effect === "write" || effect === "destructive" ? effect : "unknown";
@@ -39439,7 +39220,7 @@ async function computePage(deps, input, sources, remoteNodes, deadline, expiresA
39439
39220
  await assertFederatedCapability(deps, input, node, headers, deadline);
39440
39221
  return null;
39441
39222
  } catch (error) {
39442
- const status = error instanceof TBError && error.message.includes("unsupported") ? "unsupported" : controlledStatus(error);
39223
+ const status = isTBError(error) && error.message.includes("unsupported") ? "unsupported" : controlledStatus(error);
39443
39224
  return { error, source, status };
39444
39225
  }
39445
39226
  }
@@ -39447,7 +39228,7 @@ async function computePage(deps, input, sources, remoteNodes, deadline, expiresA
39447
39228
  for (const failure of capabilityFailures) {
39448
39229
  if (failure === null) continue;
39449
39230
  if (!initial) {
39450
- if (failure.error instanceof TBError && failure.error.code === "invalid_argument") {
39231
+ if (isTBError(failure.error) && failure.error.code === "invalid_argument") {
39451
39232
  throw failure.error;
39452
39233
  }
39453
39234
  throw failure.status === "unsupported" ? invalidContinuation() : retryableContinuationError();
@@ -39610,9 +39391,6 @@ async function executeFederatedSearch(deps, input) {
39610
39391
  "recursive search requires explicit TB_INSTANCE_ID and atomic StateStore CAS"
39611
39392
  );
39612
39393
  }
39613
- if ((input.request.opts?.mode ?? "keyword") !== "keyword") {
39614
- throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
39615
- }
39616
39394
  const wireOpts = input.request.opts ?? {};
39617
39395
  const constraints = normalizeToolSearchOptions({
39618
39396
  ...wireOpts.effects === void 0 ? {} : { effects: wireOpts.effects },
@@ -39716,7 +39494,7 @@ async function executeFederatedSearch(deps, input) {
39716
39494
  requestDigest
39717
39495
  });
39718
39496
  } catch (error) {
39719
- if (!(error instanceof TBError) || error.code !== "invalid_argument") throw error;
39497
+ if (!isTBError(error) || error.code !== "invalid_argument") throw error;
39720
39498
  }
39721
39499
  if (next !== void 0) assertNextRecord(current, next, digest);
39722
39500
  const cachedNext = next !== void 0;
@@ -39925,13 +39703,6 @@ function registerSearchRoute(app, env) {
39925
39703
  );
39926
39704
  }
39927
39705
  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
39706
  const explicitFederation = request.opts?.federation;
39936
39707
  const cursor = request.opts?.cursor;
39937
39708
  const federation = explicitFederation ?? (cursor !== void 0 && !isFederatedSearchSessionHandle(cursor) ? "local" : capabilities.includes("search:federated") ? "recursive" : "local");
@@ -39941,9 +39712,6 @@ function registerSearchRoute(app, env) {
39941
39712
  "recursive search requires capability 'search:federated'"
39942
39713
  );
39943
39714
  }
39944
- if (federation === "recursive" && mode === "semantic") {
39945
- throw new TBError("invalid_argument", "semantic search cannot use recursive federation");
39946
- }
39947
39715
  const normalizedRequest = {
39948
39716
  ...request,
39949
39717
  opts: { ...request.opts ?? {}, federation }
@@ -40011,7 +39779,7 @@ async function helpModelFor(node, registry, ctx, builtins, deps, opts) {
40011
39779
  }
40012
39780
  if (node.kind === "mcp" || node.kind === "http" || node.kind === "tool") {
40013
39781
  const provider = await providerFor(node, ctx, deps);
40014
- const raw = await upstreamTools(node, provider, deps, opts.refresh, opts.now);
39782
+ const raw = await upstreamTools(node, provider, deps, opts.refresh, opts.now, opts.searchSync);
40015
39783
  const { exposed } = virtualizeTools(node.virtualize, raw);
40016
39784
  return toolsToHelpModel(
40017
39785
  node.path,
@@ -40081,7 +39849,8 @@ async function commandHelpModelFor(registry, ctx, builtins, deps, path, opts) {
40081
39849
  const full = await helpModelFor(node, registry, ctx, builtins, deps, {
40082
39850
  refresh: opts.refresh,
40083
39851
  schemas: true,
40084
- now: (/* @__PURE__ */ new Date()).toISOString()
39852
+ now: (/* @__PURE__ */ new Date()).toISOString(),
39853
+ searchSync: opts.searchSync
40085
39854
  });
40086
39855
  const cmd = full.cmds.find((c) => c.name === command);
40087
39856
  if (cmd === void 0) return null;
@@ -40127,20 +39896,8 @@ var toolSpecCommand = (node, tool, providerBacked = false) => {
40127
39896
  ...tool.confirm === true ? { confirm: true } : {}
40128
39897
  };
40129
39898
  };
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
39899
  function mcpBridgeFor(c, env, app) {
40143
- const { builtinsOf, deps, globalSearchCapabilities } = env;
39900
+ const { builtinsOf, deps, globalSearchCapabilities, searchSync } = env;
40144
39901
  const ctx = c.get("ctx");
40145
39902
  const registry = new NodeRegistryStore(c.get("store"));
40146
39903
  let remoteRequests = 0;
@@ -40159,7 +39916,7 @@ function mcpBridgeFor(c, env, app) {
40159
39916
  additionalProperties: false,
40160
39917
  properties: {
40161
39918
  query: { type: "string", minLength: 1 },
40162
- mode: { type: "string", enum: ["keyword", "semantic"] },
39919
+ mode: { type: "string", enum: ["keyword"] },
40163
39920
  limit: { type: "integer", minimum: 1, maximum: 200 },
40164
39921
  cursor: { type: "string", minLength: 1 },
40165
39922
  detail: { type: "string", enum: ["compact", "full"], default: "compact" },
@@ -40227,6 +39984,10 @@ function mcpBridgeFor(c, env, app) {
40227
39984
  };
40228
39985
  const remoteHelp = async (path) => {
40229
39986
  takeRemoteRequest();
39987
+ const projector = await remotePathProjectorIfMatch(registry, path);
39988
+ if (projector === null) {
39989
+ throw remoteProtocolError(`remote ~help path '${path}' lost its mount owner`);
39990
+ }
40230
39991
  const headers = new Headers(c.req.raw.headers);
40231
39992
  headers.set("accept", "application/json");
40232
39993
  const response = await remotePassthroughIfMatch(
@@ -40241,28 +40002,11 @@ function mcpBridgeFor(c, env, app) {
40241
40002
  if (response === null || !response.ok) {
40242
40003
  throw new TBError("unavailable", `remote ~help failed for '${path}'`, { retryable: true });
40243
40004
  }
40244
- const model = await response.json().catch(() => null);
40245
- if (model === null || !Array.isArray(model.cmds) || typeof model.node?.path !== "string") {
40005
+ const parsed = helpJsonSchema.safeParse(await response.json().catch(() => null));
40006
+ if (!parsed.success) {
40246
40007
  throw new TBError("unavailable", `remote ~help returned invalid JSON for '${path}'`);
40247
40008
  }
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;
40009
+ return projector.projectHelp(parsed.data, path, ctx);
40266
40010
  };
40267
40011
  const remotePaths = async (root) => {
40268
40012
  const found = [];
@@ -40311,13 +40055,10 @@ function mcpBridgeFor(c, env, app) {
40311
40055
  if (check(ctx, path, command.scope).allow) {
40312
40056
  let detailed = command;
40313
40057
  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);
40058
+ const detail = await remoteHelp(command.path.replace(/^\/+/, ""));
40318
40059
  detailed = detail.cmds.find((item) => item.name === command.name) ?? command;
40319
40060
  }
40320
- result.push(remoteCommand(path, model2, detailed));
40061
+ result.push(mcpCommand(path, model2.node.description, detailed));
40321
40062
  }
40322
40063
  }
40323
40064
  }
@@ -40333,7 +40074,7 @@ function mcpBridgeFor(c, env, app) {
40333
40074
  if ((node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config !== void 0) {
40334
40075
  if (!check(ctx, node.path, "call").allow) continue;
40335
40076
  const provider = await providerFor(node, ctx, deps);
40336
- const raw = await upstreamTools(node, provider, deps, false, now);
40077
+ const raw = await upstreamTools(node, provider, deps, false, now, searchSync);
40337
40078
  const { exposed } = virtualizeTools(node.virtualize, raw);
40338
40079
  result.push(...exposed.map((tool) => toolSpecCommand(node, tool, true)));
40339
40080
  continue;
@@ -40341,7 +40082,8 @@ function mcpBridgeFor(c, env, app) {
40341
40082
  const model = await helpModelFor(node, registry, ctx, builtinsOf(c.get("store")), deps, {
40342
40083
  includeDirectUpload: check(ctx, node.path, "write").allow,
40343
40084
  refresh: false,
40344
- now
40085
+ now,
40086
+ searchSync
40345
40087
  });
40346
40088
  for (const command of model.cmds) {
40347
40089
  if (check(ctx, node.path, command.scope).allow) {
@@ -40444,7 +40186,7 @@ function mcpBridgeFor(c, env, app) {
40444
40186
  throw TBError.notFound("not found");
40445
40187
  }
40446
40188
  const provider = await providerFor(node, ctx, deps);
40447
- const raw = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString());
40189
+ const raw = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
40448
40190
  const upstreamName = resolveUpstreamTool(node.virtualize, raw, tool.toolName);
40449
40191
  const result2 = await provider.call(upstreamName, args);
40450
40192
  return {
@@ -40606,7 +40348,7 @@ async function handleInvoke(c, env) {
40606
40348
  }
40607
40349
  const args2 = await readInvokeBody();
40608
40350
  const provider = await providerFor(node, ctx, deps);
40609
- const tools = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString());
40351
+ const tools = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString(), searchSync);
40610
40352
  const upstreamName = resolveUpstreamTool(node.virtualize, tools, command);
40611
40353
  const result2 = await provider.call(upstreamName, args2);
40612
40354
  return renderResult(result2.content, negotiate(c.req.header("accept")));
@@ -40670,7 +40412,7 @@ async function handleInvoke(c, env) {
40670
40412
  if (cfg.provider !== "r2" && cfg.provider !== "s3") {
40671
40413
  const local = localContext(deps, node);
40672
40414
  if (local !== null) {
40673
- const result4 = await dispatchContextCmd2(local, command, args2);
40415
+ const result4 = await dispatchContextCmd(local, command, args2);
40674
40416
  return renderResult(result4, negotiate(c.req.header("accept")));
40675
40417
  }
40676
40418
  const { manifest, export: exported } = await requirePluginExport(
@@ -40690,11 +40432,11 @@ async function handleInvoke(c, env) {
40690
40432
  ...cfg.authRef !== void 0 ? { upstreamAuthRef: cfg.authRef } : {},
40691
40433
  ...deps.pluginBindings !== void 0 ? { bindings: deps.pluginBindings } : {}
40692
40434
  });
40693
- const result3 = await dispatchContextCmd2(provider2, command, args2);
40435
+ const result3 = await dispatchContextCmd(provider2, command, args2);
40694
40436
  return renderResult(result3, negotiate(c.req.header("accept")));
40695
40437
  }
40696
40438
  const provider = await contextProviderFor(node, cfg, deps, c.req.url);
40697
- const result2 = await dispatchContextCmd2(provider, command, args2);
40439
+ const result2 = await dispatchContextCmd(provider, command, args2);
40698
40440
  return renderResult(result2, negotiate(c.req.header("accept")));
40699
40441
  }
40700
40442
  if (node.kind === "skillhub" && node.config?.kind === "skillhub") {
@@ -40712,7 +40454,7 @@ async function handleInvoke(c, env) {
40712
40454
  throw new TBError("permission_denied", `readOnly \u6302\u8F7D\u62D2\u7EDD '${command}'`);
40713
40455
  }
40714
40456
  const provider = await skillhubProviderFor(node, cfg, deps, c.req.url);
40715
- const result2 = await dispatchSkillhubCmd2(provider, command, args2);
40457
+ const result2 = await dispatchSkillhubCmd(provider, command, args2);
40716
40458
  return renderResult(result2, negotiate(c.req.header("accept")));
40717
40459
  }
40718
40460
  if (node.kind !== "builtin" || node.config?.kind !== "builtin") {
@@ -40736,19 +40478,14 @@ async function handleInvoke(c, env) {
40736
40478
  throw new TBError("invalid_argument", "field 'path' must be a string");
40737
40479
  }
40738
40480
  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,
40481
+ await assertNodeConfigMutation({
40482
+ action: cmd === "delete" ? "delete" : "write",
40483
+ config: cfgPatch,
40742
40484
  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);
40485
+ deps,
40486
+ registry,
40487
+ targetPath
40488
+ });
40752
40489
  registryTarget = targetPath;
40753
40490
  }
40754
40491
  if (registryTarget !== void 0 && (cmd === "write" || cmd === "update")) {
@@ -40778,22 +40515,16 @@ async function handleInvoke(c, env) {
40778
40515
  return tbErrorResponse(new TBError("internal", "internal error"));
40779
40516
  }
40780
40517
  if (registryTarget !== void 0) {
40781
- await invalidateToolCache(store, registryTarget);
40782
- await invalidateMcpEra(store, registryTarget);
40783
- await invalidateMcpOAuth(store, registryTarget);
40784
- await invalidateProviderOAuth(store, registryTarget);
40518
+ await invalidateNodeDerivedState(store, registryTarget);
40785
40519
  await searchSync?.reconcileNodeQuietly(registryTarget, { marker: registryMarker });
40786
40520
  const current = await registry.get(registryTarget).catch(() => null);
40787
- if (current !== null && await refreshDynamicSearchNode(current, ctx, deps)) {
40521
+ if (current !== null && await refreshDynamicSearchNode(current, ctx, deps, searchSync)) {
40788
40522
  await searchSync?.abort(registryMarker);
40789
40523
  }
40790
40524
  }
40791
40525
  const pluginEmptyPaths = [];
40792
40526
  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);
40527
+ await invalidateNodeDerivedState(store, mount.node.path);
40797
40528
  const providerId = mount.node.config?.kind === "tool" ? mount.node.config.provider : "";
40798
40529
  const manifest = await store.get(KEY_PLUGIN + providerId);
40799
40530
  if (manifest !== null && manifest.enabled !== false) {
@@ -40841,8 +40572,7 @@ function createRouteEnv(deps) {
40841
40572
  if (!declared.has("search")) return [];
40842
40573
  return [
40843
40574
  "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"] : []
40575
+ ...deps.remote.instanceId !== void 0 && deps.state.compareAndSwap !== void 0 && deps.search?.revision !== void 0 ? ["search:federated"] : []
40846
40576
  ];
40847
40577
  };
40848
40578
  let catalogDigest;
@@ -40862,7 +40592,7 @@ function createRouteEnv(deps) {
40862
40592
 
40863
40593
  // ../app/src/routes/help.ts
40864
40594
  async function handleHelp(c, env) {
40865
- const { builtinsOf, deps } = env;
40595
+ const { builtinsOf, deps, searchSync } = env;
40866
40596
  const path = splitReserved(new URL(c.req.url).pathname, "~help");
40867
40597
  if (path === null) throw TBError.notFound("no such path");
40868
40598
  const ctx = c.get("ctx");
@@ -40901,11 +40631,12 @@ async function handleHelp(c, env) {
40901
40631
  try {
40902
40632
  node = await registry.get(path);
40903
40633
  } catch {
40904
- const toolModel = await toolHelpModelFor(c, ctx, registry, path, deps);
40634
+ const toolModel = await toolHelpModelFor(c, ctx, registry, path, deps, searchSync);
40905
40635
  if (toolModel !== null) return renderHelp(await enrichHelp(toolModel, path, store), rep);
40906
40636
  const cmdModel = await commandHelpModelFor(registry, ctx, builtins, deps, path, {
40907
40637
  refresh,
40908
- schemas: schemas2
40638
+ schemas: schemas2,
40639
+ searchSync
40909
40640
  });
40910
40641
  if (cmdModel !== null) return renderHelp(await enrichHelp(cmdModel, path, store), rep);
40911
40642
  throw TBError.notFound("not found");
@@ -40913,7 +40644,8 @@ async function handleHelp(c, env) {
40913
40644
  const model = await helpModelFor(node, registry, ctx, builtins, deps, {
40914
40645
  refresh,
40915
40646
  schemas: schemas2,
40916
- now: (/* @__PURE__ */ new Date()).toISOString()
40647
+ now: (/* @__PURE__ */ new Date()).toISOString(),
40648
+ searchSync
40917
40649
  });
40918
40650
  return renderHelp(await enrichHelp(model, path, store), rep);
40919
40651
  }
@@ -41021,6 +40753,7 @@ function createTbApp(deps) {
41021
40753
  });
41022
40754
  registerMcpRoute(app, env);
41023
40755
  registerSearchRoute(app, env);
40756
+ registerDeviceMailboxRoutes(app, env);
41024
40757
  app.get(
41025
40758
  "/system/device/ws",
41026
40759
  (c) => runHandler(async () => {
@@ -41061,13 +40794,6 @@ function createTbApp(deps) {
41061
40794
  async (c) => await runHandler(async () => {
41062
40795
  const segs = new URL(c.req.url).pathname.replace(/\/+$/, "").split("/");
41063
40796
  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
40797
  if (last === "~register") return await handleRegister(c, env);
41072
40798
  if (last === "~authorize") return await handleAuthorize(c, env);
41073
40799
  if (last === "~feedback" || segs[segs.length - 1] === "~feedback") {
@@ -41144,7 +40870,7 @@ var DeviceHub = class {
41144
40870
  this.handleUpgrade(req, socket, head).catch((err) => {
41145
40871
  rejectUpgrade(
41146
40872
  socket,
41147
- err instanceof TBError ? err : new TBError("internal", "upgrade failed")
40873
+ isTBError(err) ? err : new TBError("internal", "upgrade failed")
41148
40874
  );
41149
40875
  });
41150
40876
  });
@@ -41178,17 +40904,12 @@ var DeviceHub = class {
41178
40904
  };
41179
40905
  const conn = this.activeByDevice.get(deviceId);
41180
40906
  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;
40907
+ const authorized = conn.keyId !== void 0 && conn.mountPath !== void 0 && await reverifyDeviceAuthority({
40908
+ store: this.store,
40909
+ authorization: conn.authorization,
40910
+ keyId: conn.keyId,
40911
+ mountPath: conn.mountPath
40912
+ });
41192
40913
  if (!authorized) {
41193
40914
  conn.session.reject(TBError.unauthenticated());
41194
40915
  return offline;
@@ -41220,10 +40941,11 @@ var DeviceHub = class {
41220
40941
  }
41221
40942
  if (meta.disconnectedAt === void 0) {
41222
40943
  const now = (/* @__PURE__ */ new Date()).toISOString();
41223
- try {
41224
- await new NodeRegistryStore(this.store).setOnline(meta.mountPath, false, now);
41225
- } catch {
41226
- }
40944
+ await markDeviceDisconnected({
40945
+ mountPath: meta.mountPath,
40946
+ now,
40947
+ registry: new NodeRegistryStore(this.store)
40948
+ });
41227
40949
  await this.store.put(KEY_DEVICE_META + meta.deviceId, { ...meta, disconnectedAt: now });
41228
40950
  this.scheduleReclaim(meta.deviceId, this.reclaimSec * 1e3);
41229
40951
  } else {
@@ -41278,7 +41000,7 @@ var DeviceHub = class {
41278
41000
  close: (code) => ws.close(code),
41279
41001
  onHello: (hello) => {
41280
41002
  this.acceptHello(conn, hello).catch((err) => {
41281
- conn.session.reject(err instanceof TBError ? err : new TBError("internal", "hello \u5931\u8D25"));
41003
+ conn.session.reject(isTBError(err) ? err : new TBError("internal", "hello \u5931\u8D25"));
41282
41004
  });
41283
41005
  }
41284
41006
  },
@@ -41301,7 +41023,7 @@ var DeviceHub = class {
41301
41023
  try {
41302
41024
  conn.session.handleFrame(decodeDeviceFrame(data.toString()));
41303
41025
  } catch (err) {
41304
- conn.session.reject(err instanceof TBError ? err : new TBError("invalid_argument", "\u975E\u6CD5\u5E27"));
41026
+ conn.session.reject(isTBError(err) ? err : new TBError("invalid_argument", "\u975E\u6CD5\u5E27"));
41305
41027
  }
41306
41028
  });
41307
41029
  ws.on("close", () => {
@@ -41321,9 +41043,7 @@ var DeviceHub = class {
41321
41043
  ...this.search === void 0 ? {} : { search: this.search }
41322
41044
  });
41323
41045
  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
- );
41046
+ console.warn(deviceSearchCapacityWarning(conn.deviceId, mountPath));
41327
41047
  }
41328
41048
  const meta = { deviceId: conn.deviceId, mountPath, keyId, connectedAt: now };
41329
41049
  await this.store.put(KEY_DEVICE_META + conn.deviceId, meta);
@@ -41348,10 +41068,11 @@ var DeviceHub = class {
41348
41068
  const raw = await this.store.get(KEY_DEVICE_META + conn.deviceId);
41349
41069
  if (!isDeviceMeta(raw)) return;
41350
41070
  const now = (/* @__PURE__ */ new Date()).toISOString();
41351
- try {
41352
- await new NodeRegistryStore(this.store).setOnline(raw.mountPath, false, now);
41353
- } catch {
41354
- }
41071
+ await markDeviceDisconnected({
41072
+ mountPath: raw.mountPath,
41073
+ now,
41074
+ registry: new NodeRegistryStore(this.store)
41075
+ });
41355
41076
  await this.store.put(KEY_DEVICE_META + conn.deviceId, { ...raw, disconnectedAt: now });
41356
41077
  this.scheduleReclaim(conn.deviceId, this.reclaimSec * 1e3);
41357
41078
  }
@@ -41384,13 +41105,12 @@ var DeviceHub = class {
41384
41105
  }
41385
41106
  const raw = await this.store.get(KEY_DEVICE_META + deviceId);
41386
41107
  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);
41108
+ await reclaimDeviceSubtree({
41109
+ mountPath: raw.mountPath,
41110
+ registry: new NodeRegistryStore(this.store),
41111
+ ...this.search === void 0 ? {} : { search: this.search },
41112
+ state: this.store
41113
+ });
41394
41114
  await this.store.delete(KEY_DEVICE_META + deviceId);
41395
41115
  }
41396
41116
  /** ws 协议层探活:上一轮未回 pong → terminate(触发 close → markDisconnected)。 */
@@ -41739,7 +41459,7 @@ var FsObjectStore = class {
41739
41459
  if (!st.isFile()) continue;
41740
41460
  files.push({ key, meta: this.metaOf(key, st) });
41741
41461
  } catch (e) {
41742
- if (e instanceof TBError || isErrnoCode(e, "ENOENT")) continue;
41462
+ if (isTBError(e) || isErrnoCode(e, "ENOENT")) continue;
41743
41463
  throw e;
41744
41464
  }
41745
41465
  }
@@ -41806,7 +41526,7 @@ var FsObjectStore = class {
41806
41526
  if (real !== rootReal && !real.startsWith(rootReal + sep)) throw escapeError(key);
41807
41527
  return;
41808
41528
  } catch (e) {
41809
- if (e instanceof TBError) throw e;
41529
+ if (isTBError(e)) throw e;
41810
41530
  if (!isErrnoCode(e, "ENOENT")) throw e;
41811
41531
  const parent = dirname(probe);
41812
41532
  if (parent === probe) throw escapeError(key);
@@ -41948,7 +41668,7 @@ async function assertAncestorInRoot(root, full, key) {
41948
41668
  }
41949
41669
  return;
41950
41670
  } catch (error) {
41951
- if (error instanceof TBError) throw error;
41671
+ if (isTBError(error)) throw error;
41952
41672
  if (!isErrnoCode2(error, "ENOENT")) throw error;
41953
41673
  const parent = dirname2(probe);
41954
41674
  if (parent === probe) {
@@ -42481,7 +42201,7 @@ import { join as join4 } from "path";
42481
42201
  // package.json
42482
42202
  var package_default = {
42483
42203
  name: "@tool-bridge/server",
42484
- version: "0.20.0",
42204
+ version: "0.21.1",
42485
42205
  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
42206
  type: "module",
42487
42207
  license: "MIT",