@tool-bridge/server 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/{airtable-23JBIM6X.js → airtable-ZNWHIJMO.js} +4 -4
  2. package/dist/{alt_text_generator_ai-ADWBTCIH.js → alt_text_generator_ai-X7RMDDM5.js} +4 -4
  3. package/dist/{amap-CYQCYW2Z.js → amap-3VER75VH.js} +4 -4
  4. package/dist/{anthropic-5P62HDNR.js → anthropic-N7NTB3UC.js} +4 -4
  5. package/dist/{apify-54RZUAGW.js → apify-JB5P5AJI.js} +4 -4
  6. package/dist/{brave_search-AHJ6FTD5.js → brave_search-USL5OOBS.js} +4 -4
  7. package/dist/{chunk-SWBNHIO4.js → chunk-3WL6EXYX.js} +1 -1
  8. package/dist/{chunk-B4P6YZFS.js → chunk-6ANLYNFB.js} +1 -1
  9. package/dist/{chunk-VLOIDU5U.js → chunk-6X55ALDX.js} +3 -3
  10. package/dist/{chunk-CLEZ6YQ6.js → chunk-RVUERWUZ.js} +584 -60
  11. package/dist/{chunk-OKRZPQ3R.js → chunk-WIGQCP6O.js} +869 -74
  12. package/dist/{circleci-UM3NSQJ3.js → circleci-LBGB25LP.js} +4 -4
  13. package/dist/{clerk-FE5LQ2CZ.js → clerk-6RX37CNZ.js} +4 -4
  14. package/dist/{cloudflare_dns-KEZABKUT.js → cloudflare_dns-NQGW7FHV.js} +4 -4
  15. package/dist/{cohere-4IPJADXP.js → cohere-XPC2NAEW.js} +4 -4
  16. package/dist/{confluence-56QEKNQI.js → confluence-ZUG2LT5A.js} +4 -4
  17. package/dist/{convertapi-3QRM5K5T.js → convertapi-ZLXKWV3L.js} +3 -3
  18. package/dist/{currencyapi-L6G4OND5.js → currencyapi-T3A3HSOD.js} +4 -4
  19. package/dist/{deepseek-LGC6HBFG.js → deepseek-NJMOD23E.js} +4 -4
  20. package/dist/{docker_hub-XDMK3BIU.js → docker_hub-D2MQCSJ7.js} +4 -4
  21. package/dist/{dropbox-4QARMFZU.js → dropbox-MI6U3SOT.js} +4 -4
  22. package/dist/{dub-C6JSJKOT.js → dub-P2BYBYV4.js} +4 -4
  23. package/dist/{e2b-6A6TVACJ.js → e2b-BGKIAE54.js} +4 -4
  24. package/dist/{exa-SIWWW6AA.js → exa-EUY7VV4C.js} +4 -4
  25. package/dist/{fal_ai-3IJXOKSP.js → fal_ai-AAOG5FYJ.js} +4 -4
  26. package/dist/{feishu-L4KDA4DC.js → feishu-AN5IJYOW.js} +2 -2
  27. package/dist/{feishu_custom_bot-SDM75H4P.js → feishu_custom_bot-BWGMNVXF.js} +3 -3
  28. package/dist/{figma-6DD436DN.js → figma-SJSWYCGD.js} +4 -4
  29. package/dist/{firehydrant-SYSGYB3R.js → firehydrant-MOXK2PST.js} +4 -4
  30. package/dist/{fixer-FZG7ASTF.js → fixer-ISIDUDZU.js} +4 -4
  31. package/dist/{fly-HMRCZBUC.js → fly-U4KPSGBF.js} +4 -4
  32. package/dist/{front-FAFSD7I3.js → front-ZHM2YX2E.js} +4 -4
  33. package/dist/{geocodio-4D3TWENM.js → geocodio-SVCCAXAV.js} +4 -4
  34. package/dist/{ghost-OSO2FOZS.js → ghost-INTR7FIY.js} +4 -4
  35. package/dist/{github-OR4I4RHE.js → github-T275VVWR.js} +4 -4
  36. package/dist/{gmail-5AOPJ63E.js → gmail-KSSPOE67.js} +4 -4
  37. package/dist/{googlecalendar-SOJIJUWH.js → googlecalendar-JB5RW27L.js} +4 -4
  38. package/dist/{googledocs-QSKN5TLN.js → googledocs-G522PUGW.js} +3 -3
  39. package/dist/{grafana-R3AIZM7M.js → grafana-44ZPIV5P.js} +4 -4
  40. package/dist/{graphhopper-4CR3QOAX.js → graphhopper-2MORXLOW.js} +4 -4
  41. package/dist/index.d.ts +21 -6
  42. package/dist/index.js +3 -3
  43. package/dist/{ip2proxy-OBCG6X6J.js → ip2proxy-R4K4KEMI.js} +4 -4
  44. package/dist/{ipgeolocation_io-NHH3DTQW.js → ipgeolocation_io-H4J55OGI.js} +4 -4
  45. package/dist/{ipqualityscore-7POMCBEC.js → ipqualityscore-XGID4OHA.js} +4 -4
  46. package/dist/{jina_ai-YXVKROV4.js → jina_ai-7ZVDOR3Z.js} +4 -4
  47. package/dist/{jira-LADO3VJU.js → jira-CTIYZTT3.js} +4 -4
  48. package/dist/{langsmith-XZ6UAQFU.js → langsmith-ISP567FS.js} +4 -4
  49. package/dist/{lemlist-P74GLNE7.js → lemlist-OGKK272F.js} +4 -4
  50. package/dist/{linear-CXJ7D37A.js → linear-6JX4E3JI.js} +3 -3
  51. package/dist/{logsnag-EVVHWFLK.js → logsnag-BVB36ZCL.js} +4 -4
  52. package/dist/main.js +102 -102
  53. package/dist/{mattermost-WG2MRGBD.js → mattermost-UECVPZGY.js} +4 -4
  54. package/dist/{mem0-HQWUKG6U.js → mem0-EUA7S2OO.js} +4 -4
  55. package/dist/{memos-SQMJIFOE.js → memos-BYSBJRWZ.js} +4 -4
  56. package/dist/{metabase-E27G2DZI.js → metabase-4JXZH3YG.js} +4 -4
  57. package/dist/{mistral_ai-MIL63NIT.js → mistral_ai-2C5BTBKW.js} +4 -4
  58. package/dist/{mother_duck-AIZVCBNR.js → mother_duck-ZGNNNR3X.js} +4 -4
  59. package/dist/{ngrok-XS4DVIQS.js → ngrok-NTPGMRXI.js} +4 -4
  60. package/dist/{notes-JEOUSL2Y.js → notes-KGYXX6N6.js} +1 -1
  61. package/dist/{notion-U4EZVDW2.js → notion-EX3X2G7G.js} +4 -4
  62. package/dist/{open_exchange_rates-AZSQEYEJ.js → open_exchange_rates-2V2LIGPS.js} +4 -4
  63. package/dist/{openai-HW3SYMTQ.js → openai-KQFQPK7Z.js} +4 -4
  64. package/dist/{opengraph_io-QX74MAEL.js → opengraph_io-B63SEYJY.js} +4 -4
  65. package/dist/{openrouter-OXSKYL6N.js → openrouter-62A4H62Q.js} +4 -4
  66. package/dist/{outline-CCQAPDSW.js → outline-QC33XLCK.js} +4 -4
  67. package/dist/{paddle-6KQPSSCK.js → paddle-HDZG35Y7.js} +4 -4
  68. package/dist/{perplexity-2TXO7IQE.js → perplexity-M7LOMJWA.js} +4 -4
  69. package/dist/{pinecone-YOO7NULI.js → pinecone-Y7YCJRZM.js} +4 -4
  70. package/dist/{pipedrive-Q6RIFIFP.js → pipedrive-FLJA62UX.js} +4 -4
  71. package/dist/{polar-KUMGCES3.js → polar-AE55Q425.js} +4 -4
  72. package/dist/{posthog-VXQHBSMB.js → posthog-IAY5D3YW.js} +4 -4
  73. package/dist/{postmark-YLIPF52S.js → postmark-NMKAXDXZ.js} +4 -4
  74. package/dist/{prerender-CRY7UJN5.js → prerender-CDJM7I7N.js} +4 -4
  75. package/dist/{pubmed-3Z4T5AM7.js → pubmed-N437ZJOR.js} +3 -3
  76. package/dist/{railway-BP37IGFL.js → railway-NBWYXCUM.js} +3 -3
  77. package/dist/{readwise-HTKICF4K.js → readwise-QRO3UC3R.js} +4 -4
  78. package/dist/{render-WIVTUXJH.js → render-NEW7SROG.js} +4 -4
  79. package/dist/{replicate-JJYPA43D.js → replicate-73YQLRTY.js} +4 -4
  80. package/dist/{resend-TKHZVG7A.js → resend-R5BAZFHZ.js} +4 -4
  81. package/dist/{rootly-FRSZLG5U.js → rootly-5BLALPCS.js} +4 -4
  82. package/dist/{scrapfly-M5WXGQWE.js → scrapfly-EHFLNNUZ.js} +4 -4
  83. package/dist/{scrapingbee-PMXNLLBK.js → scrapingbee-UXLYS72S.js} +4 -4
  84. package/dist/{screenshot_fyi-HFW4TAOH.js → screenshot_fyi-KC3NLTJ6.js} +4 -4
  85. package/dist/{semantic_scholar-KXVH2VZQ.js → semantic_scholar-SA5OIUWA.js} +4 -4
  86. package/dist/{sentry-WWOT2LRX.js → sentry-BAE3XLJN.js} +4 -4
  87. package/dist/{shodan-FY635H35.js → shodan-FJR6MKKW.js} +4 -4
  88. package/dist/{shopify-HQLBVSA5.js → shopify-7OA6SLN5.js} +4 -4
  89. package/dist/{stripe-GJN2GOHB.js → stripe-MUWDZGGF.js} +3 -3
  90. package/dist/{supabase-BBNZGQU6.js → supabase-SWWGNWYR.js} +4 -4
  91. package/dist/{tavily-DPENIBOG.js → tavily-XQNJ5EDI.js} +4 -4
  92. package/dist/{telegram-L4NONG6H.js → telegram-RR6JPZOG.js} +4 -4
  93. package/dist/{telnyx-H67MAEZS.js → telnyx-7PKXOWSO.js} +4 -4
  94. package/dist/{todoist-G2BSP4DX.js → todoist-63X5IUJE.js} +4 -4
  95. package/dist/{trello-7WMVFDG2.js → trello-MOG63DAI.js} +4 -4
  96. package/dist/{turso-ZUAESFC2.js → turso-ENWTSHDL.js} +4 -4
  97. package/dist/{twilio-EYFYIX3B.js → twilio-FDXUN7VX.js} +3 -3
  98. package/dist/{umami-AXAUDUVF.js → umami-TM7PROWF.js} +4 -4
  99. package/dist/{unsplash-UCQAWPLR.js → unsplash-NWXLKSOW.js} +4 -4
  100. package/dist/{upstash_redis-ZPZ4ASU6.js → upstash_redis-CUTQ5L2N.js} +4 -4
  101. package/dist/{uptimerobot-4QVKKUD5.js → uptimerobot-NNO32M3I.js} +3 -3
  102. package/dist/{v2ex-VGEFJTYC.js → v2ex-24KMSWS7.js} +4 -4
  103. package/dist/{vercel-JME4TSCN.js → vercel-B45LPF6I.js} +4 -4
  104. package/dist/{wolfram_alpha_api-TRGU2MDG.js → wolfram_alpha_api-DKJWVR6G.js} +4 -4
  105. package/dist/{wordpress-WRUKLCZR.js → wordpress-XYFSDZXY.js} +4 -4
  106. package/dist/{workos-ZDW4EHS4.js → workos-BEM7EPHM.js} +4 -4
  107. package/dist/{zhihu-4VI67CWU.js → zhihu-CFHZ4NC7.js} +4 -4
  108. package/package.json +5 -5
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createGuardedFetch
3
- } from "./chunk-SWBNHIO4.js";
3
+ } from "./chunk-3WL6EXYX.js";
4
4
  import {
5
5
  AnnotationStore,
6
6
  CONTEXT_CAPABILITIES,
@@ -9,6 +9,7 @@ import {
9
9
  DEFAULT_STORE_DRIVER_KEY_ROOT,
10
10
  DEVICE_CALL_TIMEOUT_MS,
11
11
  DeviceGatewaySession,
12
+ DeviceMailboxService,
12
13
  FEEDBACK_HIDE_SCORE,
13
14
  FeedbackStore,
14
15
  HEADER_TB_CONTEXT,
@@ -90,6 +91,16 @@ import {
90
91
  derivePresence,
91
92
  deviceDirectoryHelpModel,
92
93
  deviceFsHelpModel,
94
+ deviceOperationClaimRequestSchema,
95
+ deviceOperationClaimResponseSchema,
96
+ deviceOperationCompleteRequestSchema,
97
+ deviceOperationDetailSchema,
98
+ deviceOperationIdentityRequestSchema,
99
+ deviceOperationListRequestSchema,
100
+ deviceOperationListResponseSchema,
101
+ deviceOperationRenewRequestSchema,
102
+ deviceOperationRenewResponseSchema,
103
+ deviceOperationSummarySchema,
93
104
  deviceShellHelpModel,
94
105
  discriminatedUnion,
95
106
  dispatchContextCmd,
@@ -185,7 +196,7 @@ import {
185
196
  url,
186
197
  validatePath,
187
198
  virtualizeTools
188
- } from "./chunk-OKRZPQ3R.js";
199
+ } from "./chunk-WIGQCP6O.js";
189
200
 
190
201
  // src/config.ts
191
202
  var DEFAULT_PORT = 8787;
@@ -1552,12 +1563,12 @@ function requireDevice(deps) {
1552
1563
  }
1553
1564
  return deps.device;
1554
1565
  }
1555
- async function invokeDevice(deps, deviceId, req) {
1566
+ async function attemptDevice(deps, deviceId, req) {
1556
1567
  const id = crypto.randomUUID();
1557
1568
  const upload = req.context === void 0 ? null : await issueDeviceCallUpload(deps, deviceId, id, req.context);
1558
- let body;
1569
+ let attempt2;
1559
1570
  try {
1560
- body = await requireDevice(deps).invoke(deviceId, {
1571
+ attempt2 = await requireDevice(deps).invoke(deviceId, {
1561
1572
  id,
1562
1573
  path: req.path,
1563
1574
  arguments: req.arguments,
@@ -1567,11 +1578,15 @@ async function invokeDevice(deps, deviceId, req) {
1567
1578
  await upload?.revoke().catch(() => {
1568
1579
  });
1569
1580
  }
1570
- if (!body || !("ok" in body)) {
1581
+ if (attempt2 === null || typeof attempt2 !== "object" || !["completed", "not_dispatched", "unknown"].includes(attempt2.disposition) || attempt2.result === null || typeof attempt2.result !== "object" || !("ok" in attempt2.result) || attempt2.disposition !== "completed" && attempt2.result.ok !== false) {
1571
1582
  throw new TBError("unavailable", "device session returned invalid result");
1572
1583
  }
1573
- if (body.ok) return body.value;
1574
- throw tbErrorFromBody(body.error);
1584
+ return attempt2;
1585
+ }
1586
+ async function invokeDevice(deps, deviceId, req) {
1587
+ const attempt2 = await attemptDevice(deps, deviceId, req);
1588
+ if (attempt2.result.ok) return attempt2.result.value;
1589
+ throw tbErrorFromBody(attempt2.result.error);
1575
1590
  }
1576
1591
  function deviceCallContextFrom(ctx) {
1577
1592
  const now = Date.now();
@@ -20633,7 +20648,7 @@ async function processDeviceHello(opts) {
20633
20648
  input,
20634
20649
  authCtx.keyId,
20635
20650
  now,
20636
- input.path === mountPath ? { online: true, lastSeenAt: now } : {}
20651
+ input.path === mountPath ? { deviceId: hello.deviceId, online: true, lastSeenAt: now } : {}
20637
20652
  );
20638
20653
  } catch (error) {
20639
20654
  await searchSync?.abort(marker);
@@ -20647,6 +20662,64 @@ async function processDeviceHello(opts) {
20647
20662
  return { mountPath, keyId: authCtx.keyId, searchIndexed };
20648
20663
  }
20649
20664
 
20665
+ // ../app/src/deviceMailbox.ts
20666
+ var KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS = "sys:device-mailbox-cleanup-progress:v1";
20667
+ function positiveInt2(value, fallback, field) {
20668
+ const actual = value ?? fallback;
20669
+ if (!Number.isSafeInteger(actual) || actual < 1) {
20670
+ throw new TBError("invalid_argument", `${field} must be a positive integer`);
20671
+ }
20672
+ return actual;
20673
+ }
20674
+ function parseProgress(value) {
20675
+ if (typeof value !== "object" || value === null || Array.isArray(value) || typeof value.cursor !== "string" || !Number.isSafeInteger(value.revision) || value.revision < 1) throw new TBError("internal", "device mailbox cleanup progress is invalid");
20676
+ return value;
20677
+ }
20678
+ function createDeviceMailboxService(deps) {
20679
+ return new DeviceMailboxService(deps.state, deps.encryptionKey);
20680
+ }
20681
+ async function cleanupDeviceMailbox(deps, opts = {}) {
20682
+ await deps.ensureReady?.();
20683
+ if (deps.state.compareAndSwap === void 0) {
20684
+ throw new TBError("unavailable", "device mailbox cleanup requires StateStore.compareAndSwap");
20685
+ }
20686
+ const mailbox = createDeviceMailboxService(deps);
20687
+ const maxPages = Math.min(positiveInt2(opts.maxPages, 8, "maxPages"), 64);
20688
+ const limit = Math.min(positiveInt2(opts.limit, 200, "limit"), 200);
20689
+ const raw = await deps.state.get(KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS);
20690
+ let progress = raw === null ? null : parseProgress(raw);
20691
+ let cursor = progress?.cursor;
20692
+ const aggregate = { deleted: 0, expired: 0, scanned: 0 };
20693
+ for (let pageNumber = 0; pageNumber < maxPages; pageNumber++) {
20694
+ const page = await mailbox.cleanup({ limit, ...cursor === void 0 ? {} : { cursor } });
20695
+ aggregate.deleted += page.deleted;
20696
+ aggregate.expired += page.expired;
20697
+ aggregate.scanned += page.scanned;
20698
+ if (page.cursor === void 0) {
20699
+ if (progress !== null) {
20700
+ await deps.state.compareAndSwap(
20701
+ KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
20702
+ progress.revision,
20703
+ null
20704
+ );
20705
+ }
20706
+ return aggregate;
20707
+ }
20708
+ const next = {
20709
+ cursor: page.cursor,
20710
+ revision: (progress?.revision ?? 0) + 1
20711
+ };
20712
+ if (!await deps.state.compareAndSwap(
20713
+ KEY_DEVICE_MAILBOX_CLEANUP_PROGRESS,
20714
+ progress?.revision ?? null,
20715
+ next
20716
+ )) return aggregate;
20717
+ progress = next;
20718
+ cursor = next.cursor;
20719
+ }
20720
+ return { ...aggregate, ...cursor === void 0 ? {} : { cursor } };
20721
+ }
20722
+
20650
20723
  // ../../node_modules/.pnpm/@modelcontextprotocol+server@2.0.0/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
20651
20724
  var __create2 = Object.create;
20652
20725
  var __defProp2 = Object.defineProperty;
@@ -36292,17 +36365,34 @@ function assertSchemaShape(value, path, depth = 0) {
36292
36365
  list.forEach((sub, i) => assertSchemaShape(sub, `${path}/${key}/${i}`, depth + 1));
36293
36366
  }
36294
36367
  }
36295
- function inputSchemaOf(raw) {
36296
- if (raw === void 0) return { type: "object", properties: {} };
36297
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
36368
+ function inputSchemaOf(raw, delivery) {
36369
+ if (raw === void 0 && delivery === void 0) return { type: "object", properties: {} };
36370
+ const source = raw ?? { type: "object", properties: {} };
36371
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
36298
36372
  throw new Error("MCP tool inputSchema must be a JSON Schema object");
36299
36373
  }
36300
- const schema = raw;
36374
+ const schema = source;
36301
36375
  if (schema.type !== void 0 && schema.type !== "object") {
36302
36376
  throw new Error("MCP tool inputSchema root type must be object");
36303
36377
  }
36304
36378
  assertSchemaShape(schema, "inputSchema");
36305
- return { ...schema, type: "object" };
36379
+ if (delivery !== "mailbox" && delivery !== "both") {
36380
+ return { ...schema, type: "object" };
36381
+ }
36382
+ const properties = schema.properties;
36383
+ if (properties !== void 0 && (properties === null || typeof properties !== "object" || Array.isArray(properties))) throw new Error("MCP tool inputSchema properties must be an object");
36384
+ return {
36385
+ ...schema,
36386
+ type: "object",
36387
+ properties: {
36388
+ ...properties ?? {},
36389
+ "~delivery": {
36390
+ type: "string",
36391
+ enum: delivery === "both" ? ["realtime", "mailbox", "fallback"] : ["mailbox", "fallback"],
36392
+ description: "Tool Bridge delivery policy; fallback prefers realtime and queues only when dispatch definitely did not occur."
36393
+ }
36394
+ }
36395
+ };
36306
36396
  }
36307
36397
  function annotationsOf(tool) {
36308
36398
  const annotations = {};
@@ -36328,7 +36418,7 @@ async function projectTools(source, validator) {
36328
36418
  HTBP ${bridge.invokePath}`;
36329
36419
  let inputSchema;
36330
36420
  try {
36331
- inputSchema = inputSchemaOf(bridge.inputSchema);
36421
+ inputSchema = inputSchemaOf(bridge.inputSchema, bridge.delivery);
36332
36422
  } catch {
36333
36423
  throw new ProtocolError3(
36334
36424
  ProtocolErrorCode2.InternalError,
@@ -36706,6 +36796,16 @@ import { Hono } from "hono";
36706
36796
  // ../core/src/protocol/openapi.ts
36707
36797
  var schemaRegistry = external_exports.registry();
36708
36798
  var schemas = {
36799
+ DeviceOperationClaimRequest: deviceOperationClaimRequestSchema,
36800
+ DeviceOperationClaimResponse: deviceOperationClaimResponseSchema,
36801
+ DeviceOperationCompleteRequest: deviceOperationCompleteRequestSchema,
36802
+ DeviceOperationDetail: deviceOperationDetailSchema,
36803
+ DeviceOperationIdentityRequest: deviceOperationIdentityRequestSchema,
36804
+ DeviceOperationListRequest: deviceOperationListRequestSchema,
36805
+ DeviceOperationListResponse: deviceOperationListResponseSchema,
36806
+ DeviceOperationRenewRequest: deviceOperationRenewRequestSchema,
36807
+ DeviceOperationRenewResponse: deviceOperationRenewResponseSchema,
36808
+ DeviceOperationSummary: deviceOperationSummarySchema,
36709
36809
  Error: tbErrorBodySchema,
36710
36810
  FeedbackDetail: feedbackDetailSchema,
36711
36811
  FeedbackList: feedbackListSchema,
@@ -36941,20 +37041,82 @@ var fixedControlPlaneOpenApi = {
36941
37041
  "x-tool-bridge-greedy-path": true
36942
37042
  }
36943
37043
  },
37044
+ "/~device/operations/get": {
37045
+ post: {
37046
+ operationId: "getDeviceOperation",
37047
+ security: bearerSecurity,
37048
+ requestBody: { required: true, content: jsonContent("DeviceOperationIdentityRequest") },
37049
+ responses: { 200: jsonResponse("Device operation detail", "DeviceOperationDetail"), ...errorResponses }
37050
+ }
37051
+ },
37052
+ "/~device/operations/list": {
37053
+ post: {
37054
+ operationId: "listDeviceOperations",
37055
+ security: bearerSecurity,
37056
+ requestBody: { required: true, content: jsonContent("DeviceOperationListRequest") },
37057
+ responses: { 200: jsonResponse("Visible device operation page", "DeviceOperationListResponse"), ...errorResponses }
37058
+ }
37059
+ },
37060
+ "/~device/operations/cancel": {
37061
+ post: {
37062
+ operationId: "cancelDeviceOperation",
37063
+ security: bearerSecurity,
37064
+ requestBody: { required: true, content: jsonContent("DeviceOperationIdentityRequest") },
37065
+ responses: { 200: jsonResponse("Cancelled or cancellation-requested operation", "DeviceOperationDetail"), ...errorResponses }
37066
+ }
37067
+ },
37068
+ "/~device/mailbox/claim": {
37069
+ post: {
37070
+ operationId: "claimDeviceOperation",
37071
+ security: bearerSecurity,
37072
+ requestBody: { required: true, content: jsonContent("DeviceOperationClaimRequest") },
37073
+ responses: { 200: jsonResponse("Claimed operation or next scan cursor", "DeviceOperationClaimResponse"), ...errorResponses }
37074
+ }
37075
+ },
37076
+ "/~device/mailbox/renew": {
37077
+ post: {
37078
+ operationId: "renewDeviceOperation",
37079
+ security: bearerSecurity,
37080
+ requestBody: { required: true, content: jsonContent("DeviceOperationRenewRequest") },
37081
+ responses: { 200: jsonResponse("Renewed operation lease", "DeviceOperationRenewResponse"), ...errorResponses }
37082
+ }
37083
+ },
37084
+ "/~device/mailbox/complete": {
37085
+ post: {
37086
+ operationId: "completeDeviceOperation",
37087
+ security: bearerSecurity,
37088
+ requestBody: { required: true, content: jsonContent("DeviceOperationCompleteRequest") },
37089
+ responses: { 200: jsonResponse("Terminal device operation", "DeviceOperationDetail"), ...errorResponses }
37090
+ }
37091
+ },
36944
37092
  "/{commandPath}": {
36945
37093
  post: {
36946
37094
  "operationId": "invoke",
36947
37095
  "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
- }],
37096
+ "parameters": [
37097
+ {
37098
+ in: "path",
37099
+ name: "commandPath",
37100
+ required: true,
37101
+ schema: { type: "string" },
37102
+ description: "Complete HTBP command path, including the command/tool leaf."
37103
+ },
37104
+ {
37105
+ in: "query",
37106
+ name: "ttlSeconds",
37107
+ schema: { type: "integer", minimum: 1 },
37108
+ description: "Mailbox operation TTL; used only when ~delivery can enqueue."
37109
+ },
37110
+ {
37111
+ in: "header",
37112
+ name: "x-tb-idempotency-key",
37113
+ schema: { type: "string", minLength: 1, maxLength: 255 },
37114
+ description: "Mailbox retry key; used only when ~delivery can enqueue."
37115
+ }
37116
+ ],
36955
37117
  "requestBody": {
36956
37118
  required: true,
36957
- description: "Raw command arguments; there is no {tool, arguments} envelope.",
37119
+ description: "Raw command arguments plus the reserved optional ~delivery control.",
36958
37120
  content: { "application/json": { schema: {} } }
36959
37121
  },
36960
37122
  "responses": {
@@ -36965,6 +37127,7 @@ var fixedControlPlaneOpenApi = {
36965
37127
  "text/markdown": { schema: { type: "string" } }
36966
37128
  }
36967
37129
  },
37130
+ 202: jsonResponse("Durably accepted device operation", "DeviceOperationDetail"),
36968
37131
  ...errorResponses
36969
37132
  },
36970
37133
  "x-tool-bridge-greedy-path": true
@@ -37157,6 +37320,239 @@ async function handleFeedbackDelete(c, env) {
37157
37320
  return feedbackJson(feedbackRemoveResponseSchema, { ok: true });
37158
37321
  }
37159
37322
 
37323
+ // ../app/src/routes/deviceMailbox.ts
37324
+ var DEVICE_MAILBOX_IDEMPOTENCY_HEADER = "x-tb-idempotency-key";
37325
+ var DEVICE_MAILBOX_HTTP_BODY_BYTES = 272 * 1024;
37326
+ var DEVICE_MAILBOX_LIST_LIMIT_DEFAULT = 50;
37327
+ var DEVICE_MAILBOX_LIST_LIMIT_MAX = 200;
37328
+ function invalidRequest(message) {
37329
+ return new TBError("invalid_argument", message);
37330
+ }
37331
+ async function mailboxJsonObject(c, opts = {}) {
37332
+ const declaredLength = Number(c.req.header("content-length"));
37333
+ if (Number.isFinite(declaredLength) && declaredLength > DEVICE_MAILBOX_HTTP_BODY_BYTES) {
37334
+ throw new TBError("rate_limited", "device mailbox request body is too large", {
37335
+ retryable: false
37336
+ });
37337
+ }
37338
+ const reader = c.req.raw.body?.getReader();
37339
+ let raw = "";
37340
+ if (reader !== void 0) {
37341
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
37342
+ let received = 0;
37343
+ try {
37344
+ while (true) {
37345
+ const chunk = await reader.read();
37346
+ if (chunk.done) break;
37347
+ received += chunk.value.byteLength;
37348
+ if (received > DEVICE_MAILBOX_HTTP_BODY_BYTES) {
37349
+ await reader.cancel().catch(() => {
37350
+ });
37351
+ throw new TBError("rate_limited", "device mailbox request body is too large", {
37352
+ retryable: false
37353
+ });
37354
+ }
37355
+ raw += decoder.decode(chunk.value, { stream: true });
37356
+ }
37357
+ raw += decoder.decode();
37358
+ } catch (cause) {
37359
+ if (cause instanceof TBError) throw cause;
37360
+ throw invalidRequest("body must be valid UTF-8 JSON");
37361
+ }
37362
+ }
37363
+ let parsed = opts.allowEmpty === true && raw === "" ? {} : null;
37364
+ if (raw !== "") {
37365
+ try {
37366
+ parsed = JSON.parse(raw);
37367
+ } catch {
37368
+ parsed = null;
37369
+ }
37370
+ }
37371
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
37372
+ throw invalidRequest("body must be a JSON object");
37373
+ }
37374
+ return parsed;
37375
+ }
37376
+ function parseBody(schema, body) {
37377
+ const parsed = schema.safeParse(body);
37378
+ if (!parsed.success) throw invalidRequest(parsed.error.issues[0]?.message ?? "invalid request");
37379
+ return parsed.data;
37380
+ }
37381
+ function deviceOperationTtlSeconds(url2) {
37382
+ for (const key of url2.searchParams.keys()) {
37383
+ if (key !== "ttlSeconds") throw invalidRequest(`unknown enqueue query parameter '${key}'`);
37384
+ }
37385
+ const value = url2.searchParams.get("ttlSeconds");
37386
+ if (value === null) return void 0;
37387
+ if (!/^[1-9]\d*$/.test(value)) throw invalidRequest("ttlSeconds must be a positive integer");
37388
+ const parsed = Number(value);
37389
+ if (!Number.isSafeInteger(parsed)) throw invalidRequest("ttlSeconds must be a safe integer");
37390
+ return parsed;
37391
+ }
37392
+ function canManage(ctx, operation) {
37393
+ return ctx.owner === operation.caller.owner || check(ctx, operation.targetPath, "admin").allow;
37394
+ }
37395
+ function assertCanManage(ctx, operation) {
37396
+ if (!canManage(ctx, operation)) throw TBError.notFound("device operation not found");
37397
+ }
37398
+ async function assertCurrentDeviceCredential(c, target) {
37399
+ const ctx = c.get("ctx");
37400
+ if (ctx.keyId !== target.deviceKeyId) {
37401
+ throw new TBError("permission_denied", "device credential does not own this operation");
37402
+ }
37403
+ let existing = null;
37404
+ try {
37405
+ existing = await new NodeRegistryStore(c.get("store")).get(target.mountPath);
37406
+ } catch {
37407
+ existing = null;
37408
+ }
37409
+ const allowed = checkRegisterPath({
37410
+ action: "write",
37411
+ existing,
37412
+ sk: {
37413
+ id: ctx.keyId,
37414
+ scopes: ctx.scopes,
37415
+ ...ctx.registerPaths === void 0 ? {} : { registerPaths: ctx.registerPaths }
37416
+ },
37417
+ targetPath: target.mountPath
37418
+ });
37419
+ if (!allowed.allow) throw allowed.error;
37420
+ }
37421
+ async function enqueueDeviceCommand(c, env, input) {
37422
+ const { arguments: args, command, marker, node } = input;
37423
+ const ctx = c.get("ctx");
37424
+ const result = await env.mailbox().enqueue({
37425
+ arguments: args,
37426
+ caller: { keyId: ctx.keyId, owner: ctx.owner },
37427
+ deviceId: marker.deviceId,
37428
+ deviceKeyId: node.registeredBy,
37429
+ mountPath: marker.mountPath,
37430
+ // 管理面与审计链必须能区分同一 tool 节点下的不同命令;权限仍按前缀 scope 判定。
37431
+ targetPath: `${node.path}/${command}`,
37432
+ path: `${relativeDevicePath(node.path, marker.mountPath)}/${command}`,
37433
+ traceId: ctx.traceId,
37434
+ ...c.req.header(DEVICE_MAILBOX_IDEMPOTENCY_HEADER) === void 0 ? {} : { idempotencyKey: c.req.header(DEVICE_MAILBOX_IDEMPOTENCY_HEADER) },
37435
+ ...input.ttlSeconds === void 0 ? {} : { ttlSeconds: input.ttlSeconds }
37436
+ });
37437
+ const response = c.json(deviceOperationDetailSchema.parse(result), 202);
37438
+ response.headers.set("x-tb-delivery", "mailbox");
37439
+ return response;
37440
+ }
37441
+ async function handleDeviceMailboxControl(c, env) {
37442
+ const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
37443
+ const body = await mailboxJsonObject(c);
37444
+ const mailbox = env.mailbox();
37445
+ const ctx = c.get("ctx");
37446
+ if (path === "/~device/operations/get") {
37447
+ const input = parseBody(deviceOperationIdentityRequestSchema, body);
37448
+ const result = await mailbox.get(input.deviceId, input.operationId);
37449
+ assertCanManage(ctx, result);
37450
+ return c.json(deviceOperationDetailSchema.parse(result));
37451
+ }
37452
+ if (path === "/~device/operations/list") {
37453
+ const input = parseBody(deviceOperationListRequestSchema, body);
37454
+ const limit = Math.min(
37455
+ input.opts?.limit ?? DEVICE_MAILBOX_LIST_LIMIT_DEFAULT,
37456
+ DEVICE_MAILBOX_LIST_LIMIT_MAX
37457
+ );
37458
+ let cursor = input.opts?.cursor;
37459
+ const items = [];
37460
+ while (items.length < limit) {
37461
+ const previous = cursor;
37462
+ const page = await mailbox.list({
37463
+ deviceId: input.deviceId,
37464
+ limit: limit - items.length,
37465
+ ...cursor === void 0 ? {} : { cursor },
37466
+ ...input.opts?.states === void 0 ? {} : { states: input.opts.states }
37467
+ });
37468
+ items.push(...page.items.filter((item) => canManage(ctx, item)));
37469
+ cursor = page.cursor;
37470
+ if (cursor === void 0) {
37471
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37472
+ }
37473
+ if (cursor === previous) {
37474
+ throw new TBError("internal", "device mailbox list cursor did not advance");
37475
+ }
37476
+ }
37477
+ const resumeCursor = cursor;
37478
+ if (resumeCursor === void 0) {
37479
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37480
+ }
37481
+ let probeCursor = resumeCursor;
37482
+ while (probeCursor !== void 0) {
37483
+ const previous = probeCursor;
37484
+ const page = await mailbox.list({
37485
+ deviceId: input.deviceId,
37486
+ cursor: probeCursor,
37487
+ limit: DEVICE_MAILBOX_LIST_LIMIT_MAX,
37488
+ ...input.opts?.states === void 0 ? {} : { states: input.opts.states }
37489
+ });
37490
+ if (page.items.some((item) => canManage(ctx, item))) {
37491
+ return c.json(deviceOperationListResponseSchema.parse({ items, cursor: resumeCursor }));
37492
+ }
37493
+ probeCursor = page.cursor;
37494
+ if (probeCursor === previous) {
37495
+ throw new TBError("internal", "device mailbox list cursor did not advance");
37496
+ }
37497
+ }
37498
+ return c.json(deviceOperationListResponseSchema.parse({ items }));
37499
+ }
37500
+ if (path === "/~device/operations/cancel") {
37501
+ const input = parseBody(deviceOperationIdentityRequestSchema, body);
37502
+ const current = await mailbox.get(input.deviceId, input.operationId);
37503
+ assertCanManage(ctx, current);
37504
+ return c.json(deviceOperationDetailSchema.parse(
37505
+ await mailbox.cancel(input.deviceId, input.operationId)
37506
+ ));
37507
+ }
37508
+ throw TBError.notFound("no such path");
37509
+ }
37510
+ async function handleDeviceMailboxData(c, env) {
37511
+ const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
37512
+ const body = await mailboxJsonObject(c);
37513
+ const mailbox = env.mailbox();
37514
+ const ctx = c.get("ctx");
37515
+ const authorize = async (target) => await assertCurrentDeviceCredential(c, target);
37516
+ if (path === "/~device/mailbox/claim") {
37517
+ const input = parseBody(deviceOperationClaimRequestSchema, body);
37518
+ return c.json(deviceOperationClaimResponseSchema.parse(await mailbox.claim({
37519
+ ...input,
37520
+ deviceKeyId: ctx.keyId,
37521
+ authorize
37522
+ })));
37523
+ }
37524
+ if (path === "/~device/mailbox/renew") {
37525
+ const input = parseBody(deviceOperationRenewRequestSchema, body);
37526
+ return c.json(deviceOperationRenewResponseSchema.parse(await mailbox.renew({
37527
+ ...input,
37528
+ deviceKeyId: ctx.keyId,
37529
+ authorize
37530
+ })));
37531
+ }
37532
+ if (path === "/~device/mailbox/complete") {
37533
+ const input = parseBody(deviceOperationCompleteRequestSchema, body);
37534
+ let completion;
37535
+ if (input.outcome === "succeeded") {
37536
+ completion = { outcome: "succeeded", result: input.result };
37537
+ } else if (input.outcome === "result_unknown") {
37538
+ completion = {
37539
+ outcome: "result_unknown",
37540
+ ...input.error === void 0 ? {} : { error: input.error }
37541
+ };
37542
+ } else {
37543
+ completion = { outcome: input.outcome, error: input.error };
37544
+ }
37545
+ return c.json(deviceOperationDetailSchema.parse(await mailbox.complete({
37546
+ deviceId: input.deviceId,
37547
+ operationId: input.operationId,
37548
+ leaseId: input.leaseId,
37549
+ deviceKeyId: ctx.keyId,
37550
+ authorize
37551
+ }, completion)));
37552
+ }
37553
+ throw TBError.notFound("no such path");
37554
+ }
37555
+
37160
37556
  // ../app/src/responses.ts
37161
37557
  function tbErrorResponse(err) {
37162
37558
  return new Response(JSON.stringify(tbErrorBodySchema.parse(err.toJSON())), {
@@ -39711,6 +40107,7 @@ var mcpCommand = (nodePath, nodeDescription, command) => {
39711
40107
  invokePath,
39712
40108
  description: command.h ?? nodeDescription,
39713
40109
  ...command.inputSchema !== void 0 ? { inputSchema: command.inputSchema } : {},
40110
+ ...command.delivery !== void 0 ? { delivery: command.delivery } : {},
39714
40111
  ...command.effect !== void 0 ? { effect: command.effect } : {},
39715
40112
  ...command.confirm === true ? { confirm: true } : {}
39716
40113
  };
@@ -39725,6 +40122,7 @@ var toolSpecCommand = (node, tool, providerBacked = false) => {
39725
40122
  ...providerBacked ? { providerBacked: true } : {},
39726
40123
  description: tool.description ?? node.description,
39727
40124
  ...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
40125
+ ...tool.delivery !== void 0 ? { delivery: tool.delivery } : {},
39728
40126
  ...tool.effect !== void 0 ? { effect: tool.effect } : {},
39729
40127
  ...tool.confirm === true ? { confirm: true } : {}
39730
40128
  };
@@ -40048,11 +40446,11 @@ function mcpBridgeFor(c, env, app) {
40048
40446
  const provider = await providerFor(node, ctx, deps);
40049
40447
  const raw = await upstreamTools(node, provider, deps, false, (/* @__PURE__ */ new Date()).toISOString());
40050
40448
  const upstreamName = resolveUpstreamTool(node.virtualize, raw, tool.toolName);
40051
- const result = await provider.call(upstreamName, args);
40449
+ const result2 = await provider.call(upstreamName, args);
40052
40450
  return {
40053
- content: result.contentBlocks ?? result.content,
40054
- ...result.isError === true ? { isError: true } : {},
40055
- ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
40451
+ content: result2.contentBlocks ?? result2.content,
40452
+ ...result2.isError === true ? { isError: true } : {},
40453
+ ...result2.structuredContent !== void 0 ? { structuredContent: result2.structuredContent } : {}
40056
40454
  };
40057
40455
  }
40058
40456
  const url2 = new URL(tool.invokePath, c.req.url);
@@ -40064,7 +40462,13 @@ function mcpBridgeFor(c, env, app) {
40064
40462
  const response = await app.request(
40065
40463
  new Request(url2, { method: "POST", headers, body: JSON.stringify(args) })
40066
40464
  );
40067
- return await resultFromResponse(response);
40465
+ const result = await resultFromResponse(response);
40466
+ if (args["~delivery"] !== void 0 && response.ok) {
40467
+ return {
40468
+ content: response.status === 202 ? { delivery: "mailbox", operation: result.content } : { delivery: "realtime", result: result.content }
40469
+ };
40470
+ }
40471
+ return result;
40068
40472
  }
40069
40473
  };
40070
40474
  }
@@ -40099,26 +40503,102 @@ async function handleInvoke(c, env) {
40099
40503
  const node = resolved.node;
40100
40504
  const command = resolved.rest;
40101
40505
  if (!check(ctx, node.path, "read").allow) throw TBError.notFound("not found");
40506
+ const toolMarker = deviceToolMarker(node);
40507
+ let invokeRequest;
40508
+ const readInvokeRequest = async () => {
40509
+ if (invokeRequest !== void 0) return await invokeRequest;
40510
+ invokeRequest = (async () => {
40511
+ const parsed = toolMarker === null ? await c.req.json().catch(() => null) : await mailboxJsonObject(c, { allowEmpty: true });
40512
+ if (parsed !== null && (typeof parsed !== "object" || Array.isArray(parsed))) {
40513
+ throw new TBError("invalid_argument", "body must be a JSON object (command arguments)");
40514
+ }
40515
+ const raw2 = { ...parsed ?? {} };
40516
+ const unknownControls = Object.keys(raw2).filter(
40517
+ (key) => key.startsWith("~") && key !== "~delivery"
40518
+ );
40519
+ if (unknownControls.length > 0) {
40520
+ throw new TBError(
40521
+ "invalid_argument",
40522
+ `unknown invocation control '${unknownControls[0]}'`
40523
+ );
40524
+ }
40525
+ const rawDelivery = raw2["~delivery"];
40526
+ delete raw2["~delivery"];
40527
+ if (rawDelivery !== void 0 && rawDelivery !== "realtime" && rawDelivery !== "mailbox" && rawDelivery !== "fallback") {
40528
+ throw new TBError(
40529
+ "invalid_argument",
40530
+ "'~delivery' must be one of realtime, mailbox, fallback"
40531
+ );
40532
+ }
40533
+ return {
40534
+ arguments: raw2,
40535
+ delivery: rawDelivery ?? "realtime",
40536
+ explicitDelivery: rawDelivery !== void 0
40537
+ };
40538
+ })();
40539
+ return await invokeRequest;
40540
+ };
40102
40541
  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)");
40542
+ const request = await readInvokeRequest();
40543
+ if (request.explicitDelivery) {
40544
+ throw new TBError(
40545
+ "invalid_argument",
40546
+ "'~delivery' is only supported by device-backed tool commands"
40547
+ );
40106
40548
  }
40107
- return parsed ?? {};
40549
+ return request.arguments;
40108
40550
  };
40109
- const toolMarker = deviceToolMarker(node);
40110
40551
  if (toolMarker !== null) {
40111
40552
  if (!check(ctx, node.path, "call").allow) {
40112
40553
  throw new TBError("permission_denied", `no scope grants 'call' on '${node.path}'`);
40113
40554
  }
40114
- const args2 = await readInvokeBody();
40115
- const result2 = await invokeDevice(deps, toolMarker.deviceId, {
40555
+ const declared = toolMarker.cmds?.find((item) => item.name.toLowerCase() === command);
40556
+ if (toolMarker.cmds !== void 0 && declared === void 0) throw TBError.notFound("not found");
40557
+ const request = await readInvokeRequest();
40558
+ const capability = declared?.delivery ?? "realtime";
40559
+ const canRealtime = capability === "realtime" || capability === "both";
40560
+ const canMailbox = capability === "mailbox" || capability === "both";
40561
+ if (request.delivery === "realtime" && !canRealtime) {
40562
+ throw new TBError("invalid_argument", "device command does not support realtime delivery");
40563
+ }
40564
+ if (request.delivery === "mailbox" && !canMailbox) {
40565
+ throw new TBError("invalid_argument", "device command does not support mailbox delivery");
40566
+ }
40567
+ if (request.delivery === "fallback" && !canMailbox) {
40568
+ throw new TBError("invalid_argument", "device command does not support mailbox fallback");
40569
+ }
40570
+ const ttlSeconds = request.delivery === "realtime" ? void 0 : deviceOperationTtlSeconds(new URL(c.req.url));
40571
+ const enqueue = async () => await enqueueDeviceCommand(c, env, {
40572
+ arguments: request.arguments,
40573
+ command,
40574
+ marker: toolMarker,
40575
+ node,
40576
+ ...ttlSeconds === void 0 ? {} : { ttlSeconds }
40577
+ });
40578
+ if (request.delivery === "mailbox" || !canRealtime) return await enqueue();
40579
+ if (request.delivery === "fallback" && deps.device === void 0) return await enqueue();
40580
+ const attempt2 = await attemptDevice(deps, toolMarker.deviceId, {
40116
40581
  // 帧 path 含命令叶子段:<mount 相对路径>/<命令>。
40117
40582
  path: `${relativeDevicePath(node.path, toolMarker.mountPath)}/${command}`,
40118
- arguments: args2,
40583
+ arguments: request.arguments,
40119
40584
  context: deviceCallContextFrom(ctx)
40120
40585
  });
40121
- return renderResult(result2, negotiate(c.req.header("accept")));
40586
+ if (attempt2.disposition === "not_dispatched" && request.delivery === "fallback") {
40587
+ return await enqueue();
40588
+ }
40589
+ if (attempt2.result.ok) {
40590
+ const response = renderResult(attempt2.result.value, negotiate(c.req.header("accept")));
40591
+ response.headers.set("x-tb-delivery", "realtime");
40592
+ return response;
40593
+ }
40594
+ if (attempt2.disposition === "unknown" && request.delivery === "fallback") {
40595
+ throw new TBError(
40596
+ attempt2.result.error.code,
40597
+ `${attempt2.result.error.message}; delivery outcome is unknown and was not enqueued`,
40598
+ { retryable: false }
40599
+ );
40600
+ }
40601
+ throw tbErrorFromBody(attempt2.result.error);
40122
40602
  }
40123
40603
  if ((node.kind === "mcp" || node.kind === "http" || node.kind === "tool") && node.config !== void 0) {
40124
40604
  if (!check(ctx, node.path, "call").allow) {
@@ -40372,7 +40852,12 @@ function createRouteEnv(deps) {
40372
40852
  catalogDigest ??= catalogSetDigest(catalog);
40373
40853
  return catalogDigest;
40374
40854
  };
40375
- return { builtinsOf, deps, globalSearchCapabilities, pluginCatalogDigest, searchSync };
40855
+ let mailboxInstance;
40856
+ const mailbox = () => {
40857
+ mailboxInstance ??= createDeviceMailboxService(deps);
40858
+ return mailboxInstance;
40859
+ };
40860
+ return { builtinsOf, deps, globalSearchCapabilities, mailbox, pluginCatalogDigest, searchSync };
40376
40861
  }
40377
40862
 
40378
40863
  // ../app/src/routes/help.ts
@@ -40576,6 +41061,13 @@ function createTbApp(deps) {
40576
41061
  async (c) => await runHandler(async () => {
40577
41062
  const segs = new URL(c.req.url).pathname.replace(/\/+$/, "").split("/");
40578
41063
  const last = segs.pop() ?? "";
41064
+ const pathname = new URL(c.req.url).pathname.replace(/\/+$/, "");
41065
+ if (pathname.startsWith("/~device/operations/")) {
41066
+ return await handleDeviceMailboxControl(c, env);
41067
+ }
41068
+ if (pathname.startsWith("/~device/mailbox/")) {
41069
+ return await handleDeviceMailboxData(c, env);
41070
+ }
40579
41071
  if (last === "~register") return await handleRegister(c, env);
40580
41072
  if (last === "~authorize") return await handleAuthorize(c, env);
40581
41073
  if (last === "~feedback" || segs[segs.length - 1] === "~feedback") {
@@ -40666,7 +41158,10 @@ var DeviceHub = class {
40666
41158
  */
40667
41159
  async invoke(deviceId, req) {
40668
41160
  if (this.activeByDevice.has(deviceId)) return await this.invokeLocal(deviceId, req);
40669
- const offline = { ok: false, error: TBError.deviceOffline().toJSON() };
41161
+ const offline = {
41162
+ disposition: "not_dispatched",
41163
+ result: { ok: false, error: TBError.deviceOffline().toJSON() }
41164
+ };
40670
41165
  if (this.router === void 0) return offline;
40671
41166
  return await this.router.forward(deviceId, req) ?? offline;
40672
41167
  }
@@ -40677,7 +41172,10 @@ var DeviceHub = class {
40677
41172
  * 那会再次查路由并转发,在两个副本间弹来弹去。这里没有连接就直接 offline。
40678
41173
  */
40679
41174
  async invokeLocal(deviceId, req) {
40680
- const offline = { ok: false, error: TBError.deviceOffline().toJSON() };
41175
+ const offline = {
41176
+ disposition: "not_dispatched",
41177
+ result: { ok: false, error: TBError.deviceOffline().toJSON() }
41178
+ };
40681
41179
  const conn = this.activeByDevice.get(deviceId);
40682
41180
  if (conn === void 0) return offline;
40683
41181
  const authCtx = await identify(this.store, conn.authorization, (/* @__PURE__ */ new Date()).toISOString());
@@ -40696,7 +41194,10 @@ var DeviceHub = class {
40696
41194
  return offline;
40697
41195
  }
40698
41196
  if (this.activeByDevice.get(deviceId) !== conn) return offline;
40699
- return await new Promise((resolve3) => conn.session.call(req, resolve3));
41197
+ return await new Promise((resolve3) => conn.session.call(
41198
+ req,
41199
+ (result, disposition) => resolve3({ disposition, result })
41200
+ ));
40700
41201
  }
40701
41202
  /** DeviceChannel.ws:Node 宿主的升级在 http 层处理,永不应命中此路由。 */
40702
41203
  async ws() {
@@ -40973,7 +41474,13 @@ var DeviceRouter = class _DeviceRouter {
40973
41474
  return await new Promise((resolve3) => {
40974
41475
  const timer = setTimeout(() => {
40975
41476
  this.pending.delete(correlationId);
40976
- resolve3({ ok: false, error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u8D85\u65F6").toJSON() });
41477
+ resolve3({
41478
+ disposition: "unknown",
41479
+ result: {
41480
+ ok: false,
41481
+ error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u8D85\u65F6").toJSON()
41482
+ }
41483
+ });
40977
41484
  }, this.opts.forwardTimeoutMs ?? DEVICE_FORWARD_TIMEOUT_MS);
40978
41485
  timer.unref?.();
40979
41486
  this.pending.set(correlationId, { resolve: resolve3, timer });
@@ -40983,8 +41490,11 @@ var DeviceRouter = class _DeviceRouter {
40983
41490
  this.pending.delete(correlationId);
40984
41491
  clearTimeout(waiter.timer);
40985
41492
  waiter.resolve({
40986
- ok: false,
40987
- error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u5931\u8D25").toJSON()
41493
+ disposition: "unknown",
41494
+ result: {
41495
+ ok: false,
41496
+ error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u5931\u8D25").toJSON()
41497
+ }
40988
41498
  });
40989
41499
  });
40990
41500
  });
@@ -40994,7 +41504,13 @@ var DeviceRouter = class _DeviceRouter {
40994
41504
  this.renewTimer = void 0;
40995
41505
  for (const [, waiter] of this.pending) {
40996
41506
  clearTimeout(waiter.timer);
40997
- waiter.resolve({ ok: false, error: new TBError("unavailable", "\u7F51\u5173\u6B63\u5728\u5173\u95ED").toJSON() });
41507
+ waiter.resolve({
41508
+ disposition: "unknown",
41509
+ result: {
41510
+ ok: false,
41511
+ error: new TBError("unavailable", "\u7F51\u5173\u6B63\u5728\u5173\u95ED").toJSON()
41512
+ }
41513
+ });
40998
41514
  }
40999
41515
  this.pending.clear();
41000
41516
  await Promise.all([...this.owned].map(async (id) => await this.release(id)));
@@ -41023,7 +41539,13 @@ var DeviceRouter = class _DeviceRouter {
41023
41539
  try {
41024
41540
  result = await this.opts.onLocalCall(call.deviceId, call.req);
41025
41541
  } catch {
41026
- result = { ok: false, error: new TBError("internal", "\u8BBE\u5907\u8C03\u7528\u6267\u884C\u5931\u8D25").toJSON() };
41542
+ result = {
41543
+ disposition: "unknown",
41544
+ result: {
41545
+ ok: false,
41546
+ error: new TBError("internal", "\u8BBE\u5907\u8C03\u7528\u6267\u884C\u5931\u8D25").toJSON()
41547
+ }
41548
+ };
41027
41549
  }
41028
41550
  const reply = { correlationId: call.correlationId, result };
41029
41551
  await this.backend.publish(deviceReplyChannel(call.replyTo), JSON.stringify(reply)).catch(() => {
@@ -41330,6 +41852,7 @@ var commandSchema = external_exports.strictObject({
41330
41852
  argv: external_exports.array(external_exports.union([external_exports.string(), argumentSchema])).optional(),
41331
41853
  cwd: external_exports.string().min(1).optional(),
41332
41854
  effect: external_exports.enum(["read", "write", "destructive"]),
41855
+ delivery: external_exports.enum(["realtime", "mailbox", "both"]).optional(),
41333
41856
  confirm: external_exports.boolean().optional(),
41334
41857
  timeoutMs: external_exports.number().int().positive().max(SHELL_EXEC_DEFAULT_TIMEOUT_MS).optional(),
41335
41858
  maxOutputBytes: external_exports.number().int().positive().max(STRUCTURED_COMMAND_MAX_OUTPUT_BYTES).optional(),
@@ -41958,7 +42481,7 @@ import { join as join4 } from "path";
41958
42481
  // package.json
41959
42482
  var package_default = {
41960
42483
  name: "@tool-bridge/server",
41961
- version: "0.19.0",
42484
+ version: "0.20.0",
41962
42485
  description: "tool-bridge self-hosted server: run the HTBP gateway on Node with SQLite or PostgreSQL persistence, filesystem object storage and WebSocket device channel \u2014 single container via docker run",
41963
42486
  type: "module",
41964
42487
  license: "MIT",
@@ -42297,26 +42820,27 @@ function createTbServer(config) {
42297
42820
  }
42298
42821
  const app = createTbApp(deps);
42299
42822
  let server;
42300
- let storeCleanupTimer;
42301
- let storeCleanupInFlight = false;
42302
- const runStoreCleanup = async () => {
42303
- if (storeCleanupInFlight) return;
42304
- storeCleanupInFlight = true;
42823
+ let maintenanceTimer;
42824
+ let maintenanceInFlight = false;
42825
+ const runMaintenance = async () => {
42826
+ if (maintenanceInFlight) return;
42827
+ maintenanceInFlight = true;
42305
42828
  try {
42306
42829
  await cleanupDefaultStore(deps);
42830
+ if (deps.encryptionKey !== void 0) await cleanupDeviceMailbox(deps);
42307
42831
  } finally {
42308
- storeCleanupInFlight = false;
42832
+ maintenanceInFlight = false;
42309
42833
  }
42310
42834
  };
42311
42835
  const reportStoreCleanupFailure = () => {
42312
42836
  console.warn(JSON.stringify({ event: "tool_bridge_store_cleanup_failed" }));
42313
42837
  };
42314
42838
  const scheduleStoreCleanup = () => {
42315
- void runStoreCleanup().catch(reportStoreCleanupFailure);
42316
- storeCleanupTimer = setInterval(() => {
42317
- void runStoreCleanup().catch(reportStoreCleanupFailure);
42839
+ void runMaintenance().catch(reportStoreCleanupFailure);
42840
+ maintenanceTimer = setInterval(() => {
42841
+ void runMaintenance().catch(reportStoreCleanupFailure);
42318
42842
  }, config.storeCleanupIntervalSec * 1e3);
42319
- storeCleanupTimer.unref?.();
42843
+ maintenanceTimer.unref?.();
42320
42844
  };
42321
42845
  return {
42322
42846
  app,
@@ -42345,9 +42869,9 @@ function createTbServer(config) {
42345
42869
  },
42346
42870
  async close() {
42347
42871
  draining = true;
42348
- if (storeCleanupTimer !== void 0) {
42349
- clearInterval(storeCleanupTimer);
42350
- storeCleanupTimer = void 0;
42872
+ if (maintenanceTimer !== void 0) {
42873
+ clearInterval(maintenanceTimer);
42874
+ maintenanceTimer = void 0;
42351
42875
  }
42352
42876
  let closed;
42353
42877
  if (server !== void 0) {