@tool-bridge/server 0.19.0 → 0.21.0

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