granttap-mcp 0.8.18 → 0.8.19

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 (309) hide show
  1. package/README.md +24 -12
  2. package/apps/bridge/src/approvals/api.ts +306 -0
  3. package/apps/bridge/src/{approval.ts → approvals/index.ts} +74 -68
  4. package/apps/bridge/src/approvals/records.ts +378 -0
  5. package/apps/bridge/src/approvals/state.ts +2 -0
  6. package/apps/bridge/src/approvals/types.ts +76 -0
  7. package/apps/bridge/src/bin/{connect.ts → cli/connect.ts} +3 -3
  8. package/apps/bridge/src/bin/{mesh-connect.ts → cli/mesh-connect.ts} +14 -4
  9. package/apps/bridge/src/bin/{pair-uri.ts → cli/pair-uri.ts} +2 -2
  10. package/apps/bridge/src/bin/{reset.ts → cli/reset.ts} +1 -1
  11. package/apps/bridge/src/bin/{setup.ts → cli/setup.ts} +10 -10
  12. package/apps/bridge/src/bin/{claude-hook.ts → hooks/claude-hook.ts} +8 -8
  13. package/apps/bridge/src/bin/{claude-prompt-hook.ts → hooks/claude-prompt-hook.ts} +1 -1
  14. package/apps/bridge/src/bin/{codex-hook.ts → hooks/codex-hook.ts} +7 -7
  15. package/apps/bridge/src/bin/{codex-policy-hook.ts → hooks/codex-policy-hook.ts} +6 -6
  16. package/apps/bridge/src/bin/{cursor-after-shell.ts → hooks/cursor-after-shell.ts} +4 -4
  17. package/apps/bridge/src/bin/{cursor-hook.ts → hooks/cursor-hook.ts} +8 -29
  18. package/apps/bridge/src/bin/hooks/shared/cursor-output.ts +21 -0
  19. package/apps/bridge/src/bin/{cursor-mcp-hook.ts → runtime/cursor-mcp-hook.ts} +13 -34
  20. package/apps/bridge/src/bin/{monitor.ts → runtime/monitor.ts} +10 -5
  21. package/apps/bridge/src/bin/runtime/restrictions-check.ts +8 -0
  22. package/apps/bridge/src/capabilities/descriptors.ts +43 -3
  23. package/apps/bridge/src/capabilities/index.ts +42 -0
  24. package/apps/bridge/src/capabilities/metadata.ts +4 -10
  25. package/apps/bridge/src/capabilities/skill-bundle.ts +40 -0
  26. package/apps/bridge/src/capabilities/skills.ts +132 -9
  27. package/apps/bridge/src/config/{capability-policy.ts → access/capability-policy.ts} +1 -1
  28. package/apps/bridge/src/config/{pairing.ts → access/pairing.ts} +7 -8
  29. package/apps/bridge/src/config/access/write-private.ts +17 -0
  30. package/apps/bridge/src/config/commands.ts +164 -0
  31. package/apps/bridge/src/{config.ts → config/index.ts} +5 -5
  32. package/apps/bridge/src/config/{runtime.ts → runtime/index.ts} +68 -10
  33. package/apps/bridge/src/config/runtime/node-bin.ts +40 -0
  34. package/apps/bridge/src/cortex/config.ts +36 -0
  35. package/apps/bridge/src/cortex/integration.ts +202 -0
  36. package/apps/bridge/src/{attachment-store.ts → delivery/attachment-store.ts} +2 -2
  37. package/apps/bridge/src/{delivery.ts → delivery/index.ts} +1 -1
  38. package/apps/bridge/src/engine/README.md +5 -0
  39. package/apps/bridge/src/engine/{invocation-decision-parse.ts → invocation/decision-parse.ts} +1 -1
  40. package/apps/bridge/src/engine/{invocation-ingest.ts → invocation/ingest.ts} +33 -21
  41. package/apps/bridge/src/engine/{invocation-log-reader.ts → invocation/log-reader.ts} +4 -3
  42. package/apps/bridge/src/engine/{invocation-parse.ts → invocation/parse.ts} +2 -2
  43. package/apps/bridge/src/engine/{invocation-query.ts → invocation/query.ts} +9 -9
  44. package/apps/bridge/src/engine/{invocation-scope.ts → invocation/scope.ts} +5 -5
  45. package/apps/bridge/src/engine/{engine-declaration.ts → protocol/engine-declaration.ts} +2 -2
  46. package/apps/bridge/src/engine/protocol/engine-memory-protocol.ts +82 -0
  47. package/apps/bridge/src/engine/protocol/engine-protocol.ts +40 -0
  48. package/apps/bridge/src/engine/protocol/transport/framing.ts +61 -0
  49. package/apps/bridge/src/engine/protocol/transport/protocol-base.ts +23 -0
  50. package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +125 -0
  51. package/apps/bridge/src/engine/protocol/transport/result-parser.ts +250 -0
  52. package/apps/bridge/src/engine/{engine-client.ts → runtime/engine-client.ts} +6 -1
  53. package/apps/bridge/src/engine/runtime/engine-memory.ts +116 -0
  54. package/apps/bridge/src/engine/runtime/engine-projects.ts +276 -0
  55. package/apps/bridge/src/engine/{engine-supervisor.ts → runtime/engine-supervisor.ts} +4 -3
  56. package/apps/bridge/src/engine/runtime/repository-graph-jobs.ts +55 -0
  57. package/apps/bridge/src/{adapters.ts → host/adapters.ts} +2 -2
  58. package/apps/bridge/src/host/instance-epoch.ts +69 -0
  59. package/apps/bridge/src/{session-keys.ts → host/session-keys.ts} +18 -15
  60. package/apps/bridge/src/install/hooks.ts +328 -0
  61. package/apps/bridge/src/install/index.ts +26 -0
  62. package/apps/bridge/src/install/inspect.ts +340 -0
  63. package/apps/bridge/src/{launchd-safety.ts → install/launchd-safety.ts} +43 -0
  64. package/apps/bridge/src/install/monitor-helper.ts +320 -0
  65. package/apps/bridge/src/{windows-service.ts → install/windows-service.ts} +9 -3
  66. package/apps/bridge/src/machine-load/{agent-load-history.ts → host/agent-load-history.ts} +1 -1
  67. package/apps/bridge/src/machine-load/{loop.ts → host/loop.ts} +1 -1
  68. package/apps/bridge/src/machine-load/{process-sampler.ts → host/process-sampler.ts} +1 -1
  69. package/apps/bridge/src/machine-load/index.ts +4 -4
  70. package/apps/bridge/src/machine-load/{mcp-load-cache.ts → mcp/cache.ts} +2 -2
  71. package/apps/bridge/src/machine-load/{mcp-process-sampler.ts → mcp/process-sampler.ts} +1 -1
  72. package/apps/bridge/src/machine-load/{mcp-load-refresh.ts → mcp/refresh.ts} +6 -6
  73. package/apps/bridge/src/mesh/README.md +7 -0
  74. package/apps/bridge/src/mesh/{capsule.ts → admin/capsule.ts} +3 -3
  75. package/apps/bridge/src/mesh/{checkpoint.ts → admin/checkpoint.ts} +1 -1
  76. package/apps/bridge/src/mesh/{convergence.ts → admin/convergence.ts} +1 -1
  77. package/apps/bridge/src/mesh/{admin.ts → admin/index.ts} +2 -2
  78. package/apps/bridge/src/mesh/{journal.ts → admin/journal.ts} +36 -12
  79. package/apps/bridge/src/mesh/{binding-state.ts → catalog/binding-state.ts} +2 -2
  80. package/apps/bridge/src/mesh/{capability.ts → catalog/capability.ts} +4 -4
  81. package/apps/bridge/src/mesh/{catalog.ts → catalog/index.ts} +93 -92
  82. package/apps/bridge/src/mesh/catalog/manifest.ts +20 -0
  83. package/apps/bridge/src/mesh/catalog/models.ts +47 -0
  84. package/apps/bridge/src/mesh/catalog/project/capabilities.ts +56 -0
  85. package/apps/bridge/src/mesh/catalog/project/configured-mcp.ts +80 -0
  86. package/apps/bridge/src/mesh/catalog/project/observations.ts +92 -0
  87. package/apps/bridge/src/mesh/catalog/project/requests.ts +72 -0
  88. package/apps/bridge/src/mesh/context/delta.ts +19 -0
  89. package/apps/bridge/src/mesh/context/env.ts +149 -0
  90. package/apps/bridge/src/mesh/context/packet.ts +54 -0
  91. package/apps/bridge/src/mesh/context/project.ts +155 -0
  92. package/apps/bridge/src/mesh/{prompt-context.ts → context/prompt.ts} +8 -8
  93. package/apps/bridge/src/mesh/delivery/affected-recipients.ts +48 -0
  94. package/apps/bridge/src/mesh/delivery/broadcast-ledger.ts +47 -0
  95. package/apps/bridge/src/mesh/delivery/index.ts +49 -0
  96. package/apps/bridge/src/mesh/{run-digest.ts → delivery/run-digest.ts} +19 -18
  97. package/apps/bridge/src/mesh/handoff/delegation-loop.ts +46 -0
  98. package/apps/bridge/src/mesh/{handoff.ts → handoff/index.ts} +1 -1
  99. package/apps/bridge/src/mesh/handoff/invite-source.ts +25 -0
  100. package/apps/bridge/src/mesh/{other-side.ts → handoff/other-side.ts} +72 -19
  101. package/apps/bridge/src/mesh/{call-scope.ts → identity/call-scope.ts} +1 -1
  102. package/apps/bridge/src/mesh/{computer-identity.ts → identity/computer.ts} +1 -1
  103. package/apps/bridge/src/mesh/identity/device-alias.ts +37 -0
  104. package/apps/bridge/src/mesh/{endpoint.ts → identity/endpoint.ts} +14 -7
  105. package/apps/bridge/src/mesh/{local.ts → local-remote/local.ts} +2 -2
  106. package/apps/bridge/src/mesh/{remote.ts → local-remote/remote.ts} +1 -1
  107. package/apps/bridge/src/mesh/{observed-claims.ts → observed/claims.ts} +4 -4
  108. package/apps/bridge/src/mesh/{integration-map.ts → observed/integration-map.ts} +4 -10
  109. package/apps/bridge/src/mesh/restrictions/check.ts +73 -0
  110. package/apps/bridge/src/mesh/restrictions/cli.ts +40 -0
  111. package/apps/bridge/src/mesh/restrictions/evaluate.ts +100 -0
  112. package/apps/bridge/src/mesh/restrictions/index.ts +19 -0
  113. package/apps/bridge/src/mesh/restrictions/store.ts +98 -0
  114. package/apps/bridge/src/mesh/restrictions/write-gate.ts +63 -0
  115. package/apps/bridge/src/mesh/runtime/capabilities/capability-inventory.ts +9 -0
  116. package/apps/bridge/src/mesh/runtime/capabilities/execution-capabilities.ts +34 -0
  117. package/apps/bridge/src/mesh/runtime/capabilities/index.ts +2 -0
  118. package/apps/bridge/src/mesh/runtime/capabilities/session-discovery.ts +12 -0
  119. package/apps/bridge/src/mesh/{runtime-dependencies.ts → runtime/dependencies.ts} +6 -4
  120. package/apps/bridge/src/mesh/runtime/execution-policy.ts +127 -0
  121. package/apps/bridge/src/mesh/{execution-sweep.ts → runtime/execution-sweep.ts} +1 -1
  122. package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +31 -0
  123. package/apps/bridge/src/mesh/runtime/handoff.ts +204 -0
  124. package/apps/bridge/src/mesh/runtime/index.ts +334 -0
  125. package/apps/bridge/src/mesh/{worktree.ts → runtime/worktree.ts} +9 -8
  126. package/apps/bridge/src/mesh/{map.ts → snapshot/map.ts} +50 -22
  127. package/apps/bridge/src/mesh/snapshot/merge.ts +51 -0
  128. package/apps/bridge/src/mesh/{scoped-view.ts → snapshot/scoped-view.ts} +30 -6
  129. package/apps/bridge/src/mesh/snapshot/window.ts +87 -0
  130. package/apps/bridge/src/mesh/store/base.ts +375 -0
  131. package/apps/bridge/src/mesh/{store.ts → store/index.ts} +71 -351
  132. package/apps/bridge/src/mesh/store/rejoin.ts +170 -0
  133. package/apps/bridge/src/mesh/store/state.ts +165 -0
  134. package/apps/bridge/src/mesh/{store-sync.ts → store/sync.ts} +8 -6
  135. package/apps/bridge/src/mesh/{attention.ts → tasks/attention.ts} +1 -1
  136. package/apps/bridge/src/mesh/tasks/create-task.ts +84 -0
  137. package/apps/bridge/src/mesh/tasks/queue.ts +54 -0
  138. package/apps/bridge/src/mesh/{readiness.ts → tasks/readiness.ts} +1 -1
  139. package/apps/bridge/src/mesh/{task-state.ts → tasks/state.ts} +2 -2
  140. package/apps/bridge/src/monitor/graph-refresh.ts +17 -0
  141. package/apps/bridge/src/monitor/handlers/catalog.ts +63 -0
  142. package/apps/bridge/src/monitor/handlers/inbound.ts +232 -0
  143. package/apps/bridge/src/monitor/handlers/receipts.ts +77 -0
  144. package/apps/bridge/src/monitor/handlers/session-commands.ts +165 -0
  145. package/apps/bridge/src/monitor/handlers/task-create.ts +109 -0
  146. package/apps/bridge/src/monitor/handlers/user-message.ts +156 -0
  147. package/apps/bridge/src/monitor/history/pages.ts +60 -0
  148. package/apps/bridge/src/monitor/index.ts +21 -0
  149. package/apps/bridge/src/{monitor-heartbeat.ts → monitor/support/heartbeat.ts} +19 -1
  150. package/apps/bridge/src/{monitor-leadership.ts → monitor/support/leadership.ts} +59 -47
  151. package/apps/bridge/src/{monitor-publish-loop.ts → monitor/support/publish-loop.ts} +3 -1
  152. package/apps/bridge/src/monitor/support/runtime.ts +305 -0
  153. package/apps/bridge/src/{monitor-session-activity.ts → monitor/support/session-activity.ts} +2 -2
  154. package/apps/bridge/src/pairing/controllers.ts +62 -0
  155. package/apps/bridge/src/pairing/index.ts +264 -0
  156. package/apps/bridge/src/pairing/presence.ts +57 -0
  157. package/apps/bridge/src/{pair-uri-file.ts → pairing/uri-file.ts} +1 -1
  158. package/apps/bridge/src/policy/capability-fingerprint.ts +8 -5
  159. package/apps/bridge/src/policy/codex-project-ask.ts +1 -1
  160. package/apps/bridge/src/policy/decision-log.ts +1 -1
  161. package/apps/bridge/src/policy/effective-action.ts +61 -12
  162. package/apps/bridge/src/policy/governed-projects.ts +1 -1
  163. package/apps/bridge/src/{policy.ts → policy/index.ts} +13 -1
  164. package/apps/bridge/src/project-policy/apply.ts +236 -0
  165. package/apps/bridge/src/project-policy/mapping.ts +2 -2
  166. package/apps/bridge/src/project-policy/runtime.ts +17 -227
  167. package/apps/bridge/src/project-policy/types.ts +46 -0
  168. package/apps/bridge/src/{claude-bin.ts → providers/claude-bin.ts} +1 -1
  169. package/apps/bridge/src/providers/codex-bin.ts +33 -0
  170. package/apps/bridge/src/{codex-control.ts → providers/codex-control.ts} +2 -3
  171. package/apps/bridge/src/{cursor-mcp-policy.ts → providers/cursor-mcp-policy.ts} +2 -15
  172. package/apps/bridge/src/{reply.ts → reply/index.ts} +48 -25
  173. package/apps/bridge/src/reply/{attachments.ts → payload/attachments.ts} +2 -2
  174. package/apps/bridge/src/reply/{routing.ts → payload/routing.ts} +1 -1
  175. package/apps/bridge/src/reply/{process.ts → process/index.ts} +30 -8
  176. package/apps/bridge/src/reply/{provider-headless.ts → process/provider-headless.ts} +23 -10
  177. package/apps/bridge/src/sessions/cursor/activity.ts +22 -22
  178. package/apps/bridge/src/sessions/cursor/catalog.ts +111 -28
  179. package/apps/bridge/src/sessions/cursor/index.ts +12 -0
  180. package/apps/bridge/src/sessions/cursor/scan.ts +119 -31
  181. package/apps/bridge/src/sessions/cursor/transcripts.ts +21 -20
  182. package/apps/bridge/src/{sessions.ts → sessions/index.ts} +68 -37
  183. package/apps/bridge/src/sessions/scan/claude/activity.ts +276 -0
  184. package/apps/bridge/src/sessions/scan/claude/index.ts +3 -0
  185. package/apps/bridge/src/sessions/scan/claude/parse.ts +165 -0
  186. package/apps/bridge/src/sessions/scan/claude/scan.ts +36 -0
  187. package/apps/bridge/src/sessions/scan/claude/shared.ts +190 -0
  188. package/apps/bridge/src/sessions/scan/codex/activity.ts +347 -0
  189. package/apps/bridge/src/sessions/scan/codex/assemble.ts +153 -0
  190. package/apps/bridge/src/sessions/scan/codex/index.ts +3 -0
  191. package/apps/bridge/src/sessions/scan/codex/parse.ts +174 -0
  192. package/apps/bridge/src/sessions/scan/codex/scan.ts +33 -0
  193. package/apps/bridge/src/sessions/scan/codex/shared.ts +201 -0
  194. package/apps/bridge/src/sessions/{grok.ts → scan/grok.ts} +13 -13
  195. package/apps/bridge/src/sessions/{activity-helpers.ts → support/activity-helpers.ts} +44 -17
  196. package/apps/bridge/src/sessions/{capability-totals.ts → support/capability-totals.ts} +0 -0
  197. package/apps/bridge/src/sessions/{child-threads.ts → support/child-threads.ts} +7 -5
  198. package/apps/bridge/src/sessions/{common.ts → support/common.ts} +9 -7
  199. package/apps/bridge/src/sessions/{telemetry.ts → telemetry/index.ts} +9 -9
  200. package/apps/bridge/src/tools/updater.ts +9 -5
  201. package/apps/mcp/src/bin/authorize.ts +5 -6
  202. package/apps/mcp/src/bin/serve.ts +4 -5
  203. package/apps/mcp/src/bin/status.ts +2 -2
  204. package/apps/mcp/src/connection-center/README.md +3 -2
  205. package/apps/mcp/src/connection-center/enrollment.ts +44 -0
  206. package/apps/mcp/src/connection-center/state.ts +32 -9
  207. package/apps/mcp/src/connection-center/view.js +42 -10
  208. package/apps/mcp/src/connection-center/widget.html +5 -4
  209. package/apps/mcp/src/cursor-config.ts +35 -3
  210. package/apps/mcp/src/{create-server.ts → http/create-server.ts} +7 -7
  211. package/apps/mcp/src/http/http-server.ts +106 -0
  212. package/apps/mcp/src/http/mcp-handler.ts +62 -0
  213. package/apps/mcp/src/http/oauth-routes.ts +109 -0
  214. package/apps/mcp/src/http-service/common.ts +4 -0
  215. package/apps/mcp/src/{http-service.ts → http-service/index.ts} +4 -4
  216. package/apps/mcp/src/http-service/installer.ts +4 -4
  217. package/apps/mcp/src/http-service/snapshot.ts +2 -2
  218. package/apps/mcp/src/mcp-tools/README.md +2 -1
  219. package/apps/mcp/src/mcp-tools/{connect.ts → connect/index.ts} +39 -29
  220. package/apps/mcp/src/mcp-tools/{relay.ts → connect/relay.ts} +46 -13
  221. package/apps/mcp/src/mcp-tools/{connection-widget.ts → connect/widget.ts} +3 -3
  222. package/apps/mcp/src/mcp-tools/interaction/answers.ts +39 -0
  223. package/apps/mcp/src/mcp-tools/interaction/index.ts +27 -0
  224. package/apps/mcp/src/mcp-tools/interaction/notify.ts +130 -0
  225. package/apps/mcp/src/mcp-tools/interaction/publish.ts +154 -0
  226. package/apps/mcp/src/mcp-tools/interaction/questions.ts +75 -0
  227. package/apps/mcp/src/mcp-tools/{mesh-actions.ts → mesh/actions.ts} +38 -7
  228. package/apps/mcp/src/mcp-tools/mesh/current.ts +100 -0
  229. package/apps/mcp/src/mcp-tools/{mesh-relay.ts → mesh/relay.ts} +4 -4
  230. package/apps/mcp/src/mcp-tools/mesh/resource.ts +68 -0
  231. package/apps/mcp/src/mcp-tools/mesh/session-env.ts +13 -0
  232. package/apps/mcp/src/mcp-tools/mesh/skills/resource.ts +52 -0
  233. package/apps/mcp/src/mcp-tools/{mesh-tools.ts → mesh/tools.ts} +1 -1
  234. package/apps/mcp/src/{mesh-server.ts → mesh/mesh-server.ts} +1 -1
  235. package/apps/mcp/src/oauth/README.md +1 -1
  236. package/apps/mcp/src/oauth/consent/after-consent.ts +31 -0
  237. package/apps/mcp/src/oauth/{pairing-view.ts → consent/pairing-view.ts} +74 -19
  238. package/apps/mcp/src/oauth/consent/pending.ts +64 -0
  239. package/apps/mcp/src/oauth/consent/website-session.ts +180 -0
  240. package/apps/mcp/src/oauth/session/connect-snapshot.ts +91 -0
  241. package/apps/mcp/src/oauth/{loopback-origin.ts → session/loopback-origin.ts} +7 -1
  242. package/apps/mcp/src/oauth/session/mailbox-claim.ts +84 -0
  243. package/apps/mcp/src/oauth/{store.ts → session/store.ts} +4 -21
  244. package/apps/mcp/src/oauth-provider.ts +82 -21
  245. package/apps/mcp/src/server.ts +5 -3
  246. package/apps/mcp/src/{package-version.ts → status/package-version.ts} +1 -1
  247. package/apps/mcp/src/status/pairing-status.ts +71 -0
  248. package/apps/mcp/src/{provider-status.ts → status/provider-status.ts} +6 -9
  249. package/bin/granttap-mcp.mjs +26 -12
  250. package/cursor-plugin/.cursor-plugin/plugin.json +3 -3
  251. package/cursor-plugin/README.md +35 -30
  252. package/cursor-plugin/commands/connect.md +8 -14
  253. package/cursor-plugin/mcp.json +5 -2
  254. package/cursor-plugin/skills/connect/SKILL.md +19 -75
  255. package/cursor-plugin/stdio-bootstrap.cjs +60 -0
  256. package/docs/cursor-authorize.md +14 -8
  257. package/package.json +5 -4
  258. package/packages/core/crypto.ts +35 -0
  259. package/packages/core/relay-client-types.ts +4 -0
  260. package/packages/core/relay-client.ts +34 -7
  261. package/packages/core/replay-store.ts +34 -0
  262. package/packages/protocol/messages/capabilities.ts +1 -0
  263. package/packages/protocol/messages/{interaction.ts → interaction/index.ts} +39 -3
  264. package/packages/protocol/messages/{mesh.ts → mesh/events.ts} +2 -162
  265. package/packages/protocol/messages/mesh/index.ts +5 -0
  266. package/packages/protocol/messages/{mesh-invocations.ts → mesh/invocations.ts} +1 -1
  267. package/packages/protocol/messages/mesh/knowledge/index.ts +19 -0
  268. package/packages/protocol/messages/mesh/snapshot.ts +317 -0
  269. package/packages/protocol/messages/mesh/tasks.ts +117 -0
  270. package/packages/protocol/messages/pairing-join.ts +36 -0
  271. package/packages/protocol/messages/project-policy.ts +112 -1
  272. package/packages/protocol/messages/{sessions.ts → sessions/index.ts} +47 -3
  273. package/packages/protocol/schema.ts +15 -2
  274. package/apps/bridge/src/approval-state.ts +0 -717
  275. package/apps/bridge/src/capabilities.ts +0 -27
  276. package/apps/bridge/src/engine/engine-projects.ts +0 -68
  277. package/apps/bridge/src/engine/engine-protocol.ts +0 -275
  278. package/apps/bridge/src/install.ts +0 -817
  279. package/apps/bridge/src/mesh/runtime-handoff.ts +0 -186
  280. package/apps/bridge/src/mesh/runtime.ts +0 -205
  281. package/apps/bridge/src/mesh/snapshot-merge.ts +0 -32
  282. package/apps/bridge/src/mesh/store-state.ts +0 -292
  283. package/apps/bridge/src/monitor.ts +0 -793
  284. package/apps/bridge/src/pairing.ts +0 -136
  285. package/apps/bridge/src/sessions/claude.ts +0 -618
  286. package/apps/bridge/src/sessions/codex.ts +0 -781
  287. package/apps/bridge/src/sessions/cursor.ts +0 -9
  288. package/apps/mcp/src/http-server.ts +0 -249
  289. package/apps/mcp/src/mcp-tools/interaction.ts +0 -407
  290. package/apps/mcp/src/mcp-tools/mesh-resource.ts +0 -137
  291. package/apps/mcp/src/pairing-status.ts +0 -25
  292. /package/apps/bridge/src/config/{paths.ts → runtime/paths.ts} +0 -0
  293. /package/apps/bridge/src/engine/{engine-invocation-protocol.ts → protocol/engine-invocation-protocol.ts} +0 -0
  294. /package/apps/bridge/src/engine/{engine-policy-protocol.ts → protocol/engine-policy-protocol.ts} +0 -0
  295. /package/apps/bridge/src/engine/{engine-policy-types.ts → protocol/engine-policy-types.ts} +0 -0
  296. /package/apps/bridge/src/engine/{engine-health.ts → runtime/engine-health.ts} +0 -0
  297. /package/apps/bridge/src/{self-protection.ts → host/self-protection.ts} +0 -0
  298. /package/apps/bridge/src/machine-load/{disk-usage.ts → host/disk-usage.ts} +0 -0
  299. /package/apps/bridge/src/machine-load/{scan-cost.ts → mcp/scan-cost.ts} +0 -0
  300. /package/apps/bridge/src/mesh/{identity.ts → identity/index.ts} +0 -0
  301. /package/apps/bridge/src/mesh/{observed-writes.ts → observed/writes.ts} +0 -0
  302. /package/apps/bridge/src/mesh/{store-support.ts → store/support.ts} +0 -0
  303. /package/apps/bridge/src/{monitor-single-flight.ts → monitor/support/single-flight.ts} +0 -0
  304. /package/apps/bridge/src/reply/{types.ts → payload/types.ts} +0 -0
  305. /package/apps/bridge/src/reply/{cursor-agent-bin.ts → process/cursor-agent-bin.ts} +0 -0
  306. /package/apps/bridge/src/sessions/{codex-head.ts → support/codex-head.ts} +0 -0
  307. /package/apps/bridge/src/sessions/{edit-stats.ts → support/edit-stats.ts} +0 -0
  308. /package/apps/mcp/src/{mesh-stdio.ts → mesh/mesh-stdio.ts} +0 -0
  309. /package/packages/protocol/messages/{mesh-endpoint.ts → mesh/endpoint.ts} +0 -0
package/README.md CHANGED
@@ -185,6 +185,14 @@ and from then on that computer takes part in the mesh like any other: its
185
185
  chats are the Project's, its claims are seen by every other computer, and a
186
186
  Task can be handed to it.
187
187
 
188
+ A Project can pin new tasks to one confirmed computer. The endpoint id is
189
+ the stored computer identity, not the display name. Models offered on the
190
+ phone come from that host's catalog. If the pinned host is offline, the
191
+ Project either refuses the create or queues it until a deadline — it does
192
+ not silently start the task elsewhere. A command issued for a previous
193
+ instance of the computer (a restore or clone that kept pairing keys) is
194
+ rejected. A pin is not a secure VM.
195
+
188
196
  ### A claim released by the person
189
197
 
190
198
  A claim outlives an agent that crashed or was closed, and the files it names
@@ -247,16 +255,20 @@ grok plugin install granttap --trust
247
255
  ```
248
256
 
249
257
  For Cursor, use the reviewed **GrantTap** Marketplace listing when it is visible
250
- to your account, or import the public `sergii-ziborov/granttap-mcp` repository
251
- once. Reload Cursor, then open
252
- **Cursor Customize → MCPs → GrantTap → Authenticate**. Complete authorization
253
- and any one-time QR pairing on `https://granttap.com/connect`.
254
-
255
- Open GrantTap from the plugin page or ask `Show my GrantTap pairing QR.` Codex
256
- renders an interactive connection card with pairing status, a one-time QR,
257
- copy-link fallback, and confirmed reconnect controls. The same tools still
258
- return readable MCP content in clients that do not render app UI. Detailed
259
- plugin instructions are in
258
+ to your account. Reload Cursor. On this computer, run `granttap setup`. Pair
259
+ and change settings on [granttap.com/connect](https://granttap.com/connect)
260
+ and in the GrantTap app. Authenticate opens that page even when this Mac is
261
+ already paired — Approve the coding app, Reconnect, or Add another. A scan in
262
+ GrantTap authorizes a new device; a saved pairing does not skip the page.
263
+ Do not add GrantTap in Cursor Customize → MCPs. Cloud **Error · fetch
264
+ failed** is leftover HTTP at `http://127.0.0.1:17342/mcp` — a user MCP row
265
+ or an old plugin `mcp.json`. The Cursor plugin is stdio (`node -e`).
266
+
267
+ Open GrantTap from the plugin connection card. Pairing status, a one-time QR
268
+ when needed, and confirmed reconnect stay on that card, on
269
+ `granttap.com/connect`, and in the GrantTap app. Do not ask an agent to print
270
+ a pairing QR in chat. The same tools still return readable MCP content in
271
+ clients that do not render app UI. Detailed plugin instructions are in
260
272
  [`plugins/granttap/README.md`](plugins/granttap/README.md).
261
273
 
262
274
  For the full background helper and provider hooks, install the CLI and run
@@ -268,8 +280,8 @@ granttap setup
268
280
  ```
269
281
 
270
282
  `granttap setup` detects supported local agents, installs or repairs their
271
- hooks, installs the background helper, configures Cursor's persistent local
272
- OAuth service when Cursor is present, and starts phone pairing when run in an
283
+ hooks, installs the background helper, removes a leftover user GrantTap MCP
284
+ entry when Cursor is present, and starts phone pairing when run in an
273
285
  interactive terminal. It ends with one exact next action.
274
286
 
275
287
  Setup also declares the separately distributed GrantTap Engine, which Project
@@ -0,0 +1,306 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { RelayClient } from "../../../../packages/core/relay-client";
3
+ import {
4
+ approvalAction,
5
+ type ApprovalDecision,
6
+ type ApprovalRequest,
7
+ type ApprovalResolved,
8
+ type ApprovalsStatus,
9
+ } from "../../../../packages/protocol/schema";
10
+ import {
11
+ MAX_PENDING_AGE_MS,
12
+ MAX_RECORDS,
13
+ TERMINAL_RETENTION_MS,
14
+ claimApprovalWinner,
15
+ effectiveFingerprint,
16
+ matchesRegistration,
17
+ normalizedSessionId,
18
+ pruneRecords,
19
+ readRawRecord,
20
+ readRecordPath,
21
+ recordFiles,
22
+ recordOutcome,
23
+ recordWinner,
24
+ registrationHandle,
25
+ requestDigest,
26
+ requestFingerprint,
27
+ terminalApproval,
28
+ writeRecord,
29
+ type ApprovalAcceptance,
30
+ type ApprovalOutcome,
31
+ type ApprovalRecord,
32
+ type ApprovalRegistration,
33
+ type ApprovalRegistrationHandle,
34
+ type ApprovalTerminalState,
35
+ type PendingApprovalRegistration,
36
+ } from "./records";
37
+
38
+ export function registerPendingApproval(
39
+ request: ApprovalRequest,
40
+ now = Date.now(),
41
+ ): ApprovalRegistration {
42
+ const fingerprint = requestFingerprint(request);
43
+ const existing = readRawRecord(request.requestId);
44
+ const existingWinner = existing ? recordWinner(existing) : null;
45
+ const effectiveAt = existingWinner?.acceptedAt ?? existing?.updatedAt;
46
+ if (existing && effectiveAt != null && now - effectiveAt <= TERMINAL_RETENTION_MS) {
47
+ if (effectiveFingerprint(existing) !== fingerprint) {
48
+ return {
49
+ matched: false,
50
+ newlyRegistered: false,
51
+ reason: "conflicting_request",
52
+ };
53
+ }
54
+ return {
55
+ matched: true,
56
+ newlyRegistered: false,
57
+ handle: registrationHandle(existing),
58
+ };
59
+ }
60
+ const record: ApprovalRecord = {
61
+ request,
62
+ state: "pending",
63
+ updatedAt: now,
64
+ generation: randomUUID(),
65
+ fingerprint,
66
+ };
67
+ writeRecord(record);
68
+ return {
69
+ matched: true,
70
+ newlyRegistered: true,
71
+ handle: registrationHandle(record),
72
+ };
73
+ }
74
+
75
+ export function acceptApprovalDecision(
76
+ decision: ApprovalDecision,
77
+ now = Date.now(),
78
+ expectedRegistration?: ApprovalRegistrationHandle,
79
+ ): ApprovalAcceptance {
80
+ const record = readRawRecord(decision.requestId);
81
+ if (!record || !matchesRegistration(record, expectedRegistration)) {
82
+ return { matched: false, newlyResolved: false };
83
+ }
84
+ const requestSessionId = normalizedSessionId(record.request.sessionId);
85
+ const decisionSessionId = normalizedSessionId(decision.sessionId);
86
+ if (requestSessionId !== decisionSessionId) {
87
+ return { matched: false, newlyResolved: false };
88
+ }
89
+ const existingOutcome = recordOutcome(record);
90
+ if (existingOutcome) return acceptance(record, existingOutcome, false);
91
+ if (record.state !== "pending") return { matched: false, newlyResolved: false };
92
+ if (now - record.updatedAt > MAX_PENDING_AGE_MS) {
93
+ return markApprovalTerminal(
94
+ decision.requestId,
95
+ "expired",
96
+ {
97
+ decision: "deny",
98
+ decidedBy: "system",
99
+ note: "Approval request expired before the decision arrived",
100
+ sessionId: requestSessionId,
101
+ },
102
+ now,
103
+ expectedRegistration,
104
+ );
105
+ }
106
+ const storedDecision: ApprovalDecision = {
107
+ ...decision,
108
+ sessionId: requestSessionId,
109
+ decidedAt: decision.decidedAt || now,
110
+ };
111
+ const outcome: ApprovalOutcome = { kind: "decision", decision: storedDecision };
112
+ const claim = claimApprovalWinner(record, outcome, now);
113
+ if (!claim.winner) {
114
+ // A corrupt/unreadable winner is fail-closed: do not ACK either contender.
115
+ return { matched: false, newlyResolved: false };
116
+ }
117
+ if (!claim.won) {
118
+ return acceptance(record, claim.winner.outcome, false);
119
+ }
120
+ return acceptance(record, claim.winner.outcome, true);
121
+ }
122
+
123
+ function acceptance(
124
+ record: ApprovalRecord,
125
+ outcome: ApprovalOutcome,
126
+ newlyResolved: boolean,
127
+ ): ApprovalAcceptance {
128
+ return {
129
+ matched: true,
130
+ newlyResolved,
131
+ request: record.request,
132
+ outcome,
133
+ decision: outcome.kind === "decision" ? outcome.decision : undefined,
134
+ };
135
+ }
136
+
137
+ /** Read the exact-scope outcome durably accepted by another process. */
138
+ export function resolvedApprovalOutcome(
139
+ requestId: string,
140
+ expectedSessionId: string | null | undefined,
141
+ expectedRegistration?: ApprovalRegistrationHandle,
142
+ ): ApprovalOutcome | null {
143
+ const record = readRawRecord(requestId);
144
+ if (
145
+ !record ||
146
+ normalizedSessionId(record.request.sessionId) !== normalizedSessionId(expectedSessionId) ||
147
+ !matchesRegistration(record, expectedRegistration)
148
+ ) {
149
+ return null;
150
+ }
151
+ return recordOutcome(record);
152
+ }
153
+
154
+ /** Read a decision only when the current request generation and exact scope match. */
155
+ export function resolvedApprovalDecision(
156
+ requestId: string,
157
+ expectedSessionId: string | null | undefined,
158
+ expectedRegistration?: ApprovalRegistrationHandle,
159
+ ): ApprovalDecision | null {
160
+ const outcome = resolvedApprovalOutcome(
161
+ requestId,
162
+ expectedSessionId,
163
+ expectedRegistration,
164
+ );
165
+ return outcome?.kind === "decision" ? outcome.decision : null;
166
+ }
167
+
168
+ export function markApprovalTerminal(
169
+ requestId: string,
170
+ state: ApprovalTerminalState,
171
+ options: {
172
+ decision?: ApprovalDecision["decision"] | null;
173
+ decidedBy?: string | null;
174
+ note?: string | null;
175
+ sessionId?: string | null;
176
+ resolvedAt?: number;
177
+ } = {},
178
+ now = Date.now(),
179
+ expectedRegistration?: ApprovalRegistrationHandle,
180
+ ): ApprovalAcceptance {
181
+ const record = readRawRecord(requestId);
182
+ if (!record || !matchesRegistration(record, expectedRegistration)) {
183
+ return { matched: false, newlyResolved: false };
184
+ }
185
+ const requestSessionId = normalizedSessionId(record.request.sessionId);
186
+ if (requestSessionId !== normalizedSessionId(options.sessionId)) {
187
+ return { matched: false, newlyResolved: false };
188
+ }
189
+ const existingOutcome = recordOutcome(record);
190
+ if (existingOutcome) return acceptance(record, existingOutcome, false);
191
+ if (record.state !== "pending") return { matched: false, newlyResolved: false };
192
+ const resolved = terminalApproval(requestId, state, {
193
+ ...options,
194
+ sessionId: requestSessionId,
195
+ resolvedAt: options.resolvedAt ?? now,
196
+ });
197
+ const outcome: ApprovalOutcome = { kind: "terminal", status: state, resolved };
198
+ const claim = claimApprovalWinner(record, outcome, now);
199
+ if (!claim.winner) return { matched: false, newlyResolved: false };
200
+ if (!claim.won) return acceptance(record, claim.winner.outcome, false);
201
+ return acceptance(record, claim.winner.outcome, true);
202
+ }
203
+
204
+ export function pendingApprovalRegistrations(
205
+ now = Date.now(),
206
+ ): PendingApprovalRegistration[] {
207
+ return recordFiles()
208
+ .map(readRecordPath)
209
+ .filter((record): record is ApprovalRecord =>
210
+ record != null &&
211
+ record.state === "pending" &&
212
+ now - record.updatedAt <= MAX_PENDING_AGE_MS &&
213
+ recordWinner(record) == null,
214
+ )
215
+ .sort((a, b) => b.updatedAt - a.updatedAt)
216
+ .slice(0, 100)
217
+ .map((record) => ({
218
+ request: record.request,
219
+ handle: registrationHandle(record),
220
+ }));
221
+ }
222
+
223
+ /** Compatibility wrapper for callers that only render pending requests. */
224
+ export function pendingApprovalRequests(now = Date.now()): ApprovalRequest[] {
225
+ return pendingApprovalRegistrations(now).map((item) => item.request);
226
+ }
227
+
228
+ function approvalScopeKey(request: Pick<ApprovalRequest, "requestId" | "sessionId">): string {
229
+ return `${request.requestId}\u0000${normalizedSessionId(request.sessionId) ?? ""}`;
230
+ }
231
+
232
+ function coveredApprovalScopes(
233
+ pending: ApprovalRequest[],
234
+ now: number,
235
+ ): Array<{ requestId: string; sessionId?: string }> {
236
+ const includedPending = new Set(pending.map(approvalScopeKey));
237
+ const covered = new Map<string, { requestId: string; sessionId?: string }>();
238
+ for (const path of recordFiles()) {
239
+ const record = readRecordPath(path);
240
+ if (!record) continue;
241
+ const winner = recordWinner(record);
242
+ if (now - (winner?.acceptedAt ?? record.updatedAt) > TERMINAL_RETENTION_MS) continue;
243
+ const isLivePending =
244
+ record.state === "pending" &&
245
+ winner == null &&
246
+ now - record.updatedAt <= MAX_PENDING_AGE_MS;
247
+ // Never claim an omitted live request as closed when the wire cap is hit.
248
+ if (isLivePending && !includedPending.has(approvalScopeKey(record.request))) continue;
249
+ const scope = {
250
+ requestId: record.request.requestId,
251
+ sessionId: normalizedSessionId(record.request.sessionId),
252
+ };
253
+ covered.set(approvalScopeKey(record.request), scope);
254
+ }
255
+ return [...covered.values()].slice(0, MAX_RECORDS);
256
+ }
257
+
258
+ let lastStatusGeneratedAt = 0;
259
+
260
+ export function approvalsStatus(now = Date.now()): ApprovalsStatus {
261
+ pruneRecords(now);
262
+ const pending = pendingApprovalRequests(now);
263
+ const generatedAt = Math.max(Math.trunc(now), lastStatusGeneratedAt + 1);
264
+ lastStatusGeneratedAt = generatedAt;
265
+ return {
266
+ type: "approvals.status",
267
+ pending,
268
+ complete: false,
269
+ covered: coveredApprovalScopes(pending, now),
270
+ actions: pending.map(approvalAction),
271
+ generatedAt,
272
+ };
273
+ }
274
+
275
+ export function resolvedFromDecision(
276
+ decision: ApprovalDecision,
277
+ request?: ApprovalRequest,
278
+ ): ApprovalResolved {
279
+ return terminalApproval(decision.requestId, "applied", {
280
+ decision: decision.decision,
281
+ decidedBy: decision.decidedBy,
282
+ note: decision.note,
283
+ sessionId: request?.sessionId ?? decision.sessionId,
284
+ resolvedAt: decision.decidedAt,
285
+ });
286
+ }
287
+
288
+ export function resolvedFromOutcome(
289
+ outcome: ApprovalOutcome,
290
+ request?: ApprovalRequest,
291
+ ): ApprovalResolved {
292
+ return outcome.kind === "decision"
293
+ ? resolvedFromDecision(outcome.decision, request)
294
+ : outcome.resolved;
295
+ }
296
+
297
+ export async function sendApprovalResolved(
298
+ client: RelayClient,
299
+ payload: ApprovalResolved,
300
+ ): Promise<void> {
301
+ await client.send(payload, "phone", {
302
+ ttlMs: 15 * 60_000,
303
+ wake: false,
304
+ deliveryId: `approval-resolved-${requestDigest(payload.requestId)}`,
305
+ });
306
+ }
@@ -5,9 +5,9 @@
5
5
  * replies with a matching ApprovalDecision (or the timeout fires — fail closed,
6
6
  * i.e. deny). This is what makes a phone tap gate a real tool call.
7
7
  */
8
- import type { PeerConfig } from "../../../packages/core/relay-client";
9
- import { RelayClient } from "../../../packages/core/relay-client";
10
- import type { ApprovalDecision, ApprovalRequest, Payload } from "../../../packages/protocol/schema";
8
+ import type { PeerConfig } from "../../../../packages/core/relay-client";
9
+ import { RelayClient } from "../../../../packages/core/relay-client";
10
+ import type { ApprovalDecision, ApprovalRequest, Payload } from "../../../../packages/protocol/schema";
11
11
  import {
12
12
  acceptApprovalDecision,
13
13
  markApprovalTerminal,
@@ -16,8 +16,9 @@ import {
16
16
  resolvedFromOutcome,
17
17
  sendApprovalResolved,
18
18
  type ApprovalOutcome,
19
- } from "./approval-state";
20
- import { primeSessionKeys, sendSessionPayload } from "./session-keys";
19
+ type ApprovalRegistrationHandle,
20
+ } from "./state";
21
+ import { primeSessionKeys, sendSessionPayload } from "../host/session-keys";
21
22
 
22
23
  /**
23
24
  * Nobody answered — relay unreachable, or the phone never responded before the
@@ -77,7 +78,72 @@ export async function requestApproval(
77
78
  };
78
79
  }
79
80
  }
80
- const decisionP = new Promise<ApprovalDecision | null>((resolve) => {
81
+ const waiting = waitForDecision(client, req, timeoutMs, registrationHandle);
82
+ cancelDecisionWait = waiting.cancel;
83
+ await sendRequest(client, req, timeoutMs);
84
+ const decision = await waiting.promise;
85
+
86
+ if (decision) return decision;
87
+ // `expired`, not `system`: nobody denied anything — the phone was asleep,
88
+ // offline, or the app was killed. Hooks hand `expired` back to the agent's
89
+ // own permission prompt. Reporting it as a plain deny bricks the agent:
90
+ // every tool call stalls for the full timeout and then fails.
91
+ const expired = markApprovalTerminal(req.requestId, "expired", {
92
+ decision: "deny",
93
+ decidedBy: "expired",
94
+ note: "No response from phone before timeout",
95
+ sessionId: req.sessionId,
96
+ }, Date.now(), registrationHandle);
97
+ if (!expired.outcome) {
98
+ return {
99
+ ...failClosed(req, "No response from phone before timeout"),
100
+ decidedBy: "expired",
101
+ };
102
+ }
103
+ await sendApprovalResolved(
104
+ client,
105
+ resolvedFromOutcome(expired.outcome, expired.request),
106
+ ).catch(() => {});
107
+ return decisionFromOutcome(expired.outcome, req);
108
+ } catch (err) {
109
+ // Transport blew up mid-flight (socket dropped, peer never attached). Like a
110
+ // failed connect this is `unreachable`: no human ever saw the request.
111
+ const expired = markApprovalTerminal(req.requestId, "expired", {
112
+ decision: "deny",
113
+ decidedBy: "unreachable",
114
+ note: "Approval channel error",
115
+ sessionId: req.sessionId,
116
+ }, Date.now(), registrationHandle);
117
+ if (!expired.outcome) {
118
+ return {
119
+ ...failClosed(req, `Approval channel error: ${(err as Error).message}`),
120
+ decidedBy: "unreachable",
121
+ };
122
+ }
123
+ await sendApprovalResolved(
124
+ client,
125
+ resolvedFromOutcome(expired.outcome, expired.request),
126
+ ).catch(() => {});
127
+ return decisionFromOutcome(expired.outcome, req);
128
+ } finally {
129
+ cancelDecisionWait();
130
+ if (ownsClient) client.close();
131
+ }
132
+ }
133
+
134
+ async function sendRequest(client: RelayClient, req: ApprovalRequest, timeoutMs: number): Promise<void> {
135
+ // The id carries no task content and lets APNs answer before iOS decrypts the card.
136
+ const options = { ttlMs: timeoutMs, wake: true, deliveryId: req.requestId };
137
+ if (req.sessionId) await sendSessionPayload(client, req, req.sessionId, "phone", options);
138
+ else await client.send(req, "phone", options);
139
+ }
140
+
141
+ function waitForDecision(
142
+ client: RelayClient, req: ApprovalRequest, timeoutMs: number,
143
+ registrationHandle?: ApprovalRegistrationHandle,
144
+ ): { promise: Promise<ApprovalDecision | null>; cancel: () => void } {
145
+ let cancel = () => {};
146
+ const promise = new Promise<ApprovalDecision | null>((resolve) => {
81
147
  let settled = false;
82
148
  let durablePoll: NodeJS.Timeout | undefined;
83
149
  let off = () => {};
@@ -89,7 +155,7 @@ export async function requestApproval(
89
155
  off();
90
156
  resolve(decision);
91
157
  };
92
- cancelDecisionWait = () => finish(null);
158
+ cancel = () => finish(null);
93
159
  const timer = setTimeout(() => finish(null), timeoutMs);
94
160
  // The long-running desktop monitor may receive and durably accept the
95
161
  // same relay envelope first. Poll the shared registry so this one-shot
@@ -127,67 +193,7 @@ export async function requestApproval(
127
193
  return terminalSent;
128
194
  });
129
195
  });
130
- // The request id is random and carries no task content. Reusing it as the
131
- // opaque delivery id lets a generic APNs action answer the request even if
132
- // iOS has not yet pulled and decrypted the full card.
133
- const sendOptions = {
134
- ttlMs: timeoutMs,
135
- wake: true,
136
- deliveryId: req.requestId,
137
- };
138
- if (req.sessionId) {
139
- await sendSessionPayload(client, req, req.sessionId, "phone", sendOptions);
140
- } else {
141
- await client.send(req, "phone", sendOptions);
142
- }
143
- const decision = await decisionP;
144
-
145
- if (decision) return decision;
146
- // `expired`, not `system`: nobody denied anything — the phone was asleep,
147
- // offline, or the app was killed. Hooks hand `expired` back to the agent's
148
- // own permission prompt. Reporting it as a plain deny bricks the agent:
149
- // every tool call stalls for the full timeout and then fails.
150
- const expired = markApprovalTerminal(req.requestId, "expired", {
151
- decision: "deny",
152
- decidedBy: "expired",
153
- note: "No response from phone before timeout",
154
- sessionId: req.sessionId,
155
- }, Date.now(), registrationHandle);
156
- if (!expired.outcome) {
157
- return {
158
- ...failClosed(req, "No response from phone before timeout"),
159
- decidedBy: "expired",
160
- };
161
- }
162
- await sendApprovalResolved(
163
- client,
164
- resolvedFromOutcome(expired.outcome, expired.request),
165
- ).catch(() => {});
166
- return decisionFromOutcome(expired.outcome, req);
167
- } catch (err) {
168
- // Transport blew up mid-flight (socket dropped, peer never attached). Like a
169
- // failed connect this is `unreachable`: no human ever saw the request.
170
- const expired = markApprovalTerminal(req.requestId, "expired", {
171
- decision: "deny",
172
- decidedBy: "unreachable",
173
- note: "Approval channel error",
174
- sessionId: req.sessionId,
175
- }, Date.now(), registrationHandle);
176
- if (!expired.outcome) {
177
- return {
178
- ...failClosed(req, `Approval channel error: ${(err as Error).message}`),
179
- decidedBy: "unreachable",
180
- };
181
- }
182
- await sendApprovalResolved(
183
- client,
184
- resolvedFromOutcome(expired.outcome, expired.request),
185
- ).catch(() => {});
186
- return decisionFromOutcome(expired.outcome, req);
187
- } finally {
188
- cancelDecisionWait();
189
- if (ownsClient) client.close();
190
- }
196
+ return { promise, cancel };
191
197
  }
192
198
 
193
199
  function decisionFromOutcome(