granttap-mcp 0.8.17 → 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 (311) hide show
  1. package/README.md +50 -14
  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} +11 -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} +9 -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 +157 -15
  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 +16 -1
  39. package/apps/bridge/src/engine/invocation/decision-parse.ts +35 -0
  40. package/apps/bridge/src/engine/invocation/ingest.ts +179 -0
  41. package/apps/bridge/src/engine/invocation/log-reader.ts +72 -0
  42. package/apps/bridge/src/engine/invocation/parse.ts +120 -0
  43. package/apps/bridge/src/engine/invocation/query.ts +53 -0
  44. package/apps/bridge/src/engine/invocation/scope.ts +39 -0
  45. package/apps/bridge/src/engine/{engine-declaration.ts → protocol/engine-declaration.ts} +2 -2
  46. package/apps/bridge/src/engine/protocol/engine-invocation-protocol.ts +117 -0
  47. package/apps/bridge/src/engine/protocol/engine-memory-protocol.ts +82 -0
  48. package/apps/bridge/src/engine/protocol/engine-protocol.ts +40 -0
  49. package/apps/bridge/src/engine/protocol/transport/framing.ts +61 -0
  50. package/apps/bridge/src/engine/protocol/transport/protocol-base.ts +23 -0
  51. package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +125 -0
  52. package/apps/bridge/src/engine/protocol/transport/result-parser.ts +250 -0
  53. package/apps/bridge/src/engine/{engine-client.ts → runtime/engine-client.ts} +8 -1
  54. package/apps/bridge/src/engine/runtime/engine-memory.ts +116 -0
  55. package/apps/bridge/src/engine/runtime/engine-projects.ts +276 -0
  56. package/apps/bridge/src/engine/{engine-supervisor.ts → runtime/engine-supervisor.ts} +4 -3
  57. package/apps/bridge/src/engine/runtime/repository-graph-jobs.ts +55 -0
  58. package/apps/bridge/src/{adapters.ts → host/adapters.ts} +2 -2
  59. package/apps/bridge/src/host/instance-epoch.ts +69 -0
  60. package/apps/bridge/src/{session-keys.ts → host/session-keys.ts} +20 -16
  61. package/apps/bridge/src/install/hooks.ts +328 -0
  62. package/apps/bridge/src/install/index.ts +26 -0
  63. package/apps/bridge/src/install/inspect.ts +340 -0
  64. package/apps/bridge/src/{launchd-safety.ts → install/launchd-safety.ts} +43 -0
  65. package/apps/bridge/src/install/monitor-helper.ts +320 -0
  66. package/apps/bridge/src/{windows-service.ts → install/windows-service.ts} +9 -3
  67. package/apps/bridge/src/machine-load/{agent-load-history.ts → host/agent-load-history.ts} +1 -1
  68. package/apps/bridge/src/machine-load/{loop.ts → host/loop.ts} +1 -1
  69. package/apps/bridge/src/machine-load/{process-sampler.ts → host/process-sampler.ts} +1 -1
  70. package/apps/bridge/src/machine-load/index.ts +4 -4
  71. package/apps/bridge/src/machine-load/{mcp-load-cache.ts → mcp/cache.ts} +2 -2
  72. package/apps/bridge/src/machine-load/{mcp-process-sampler.ts → mcp/process-sampler.ts} +1 -1
  73. package/apps/bridge/src/machine-load/{mcp-load-refresh.ts → mcp/refresh.ts} +6 -6
  74. package/apps/bridge/src/mesh/README.md +7 -0
  75. package/apps/bridge/src/mesh/{capsule.ts → admin/capsule.ts} +3 -3
  76. package/apps/bridge/src/mesh/{checkpoint.ts → admin/checkpoint.ts} +1 -1
  77. package/apps/bridge/src/mesh/{convergence.ts → admin/convergence.ts} +1 -1
  78. package/apps/bridge/src/mesh/{admin.ts → admin/index.ts} +2 -2
  79. package/apps/bridge/src/mesh/{journal.ts → admin/journal.ts} +36 -12
  80. package/apps/bridge/src/mesh/{binding-state.ts → catalog/binding-state.ts} +2 -2
  81. package/apps/bridge/src/mesh/{capability.ts → catalog/capability.ts} +4 -4
  82. package/apps/bridge/src/mesh/{catalog.ts → catalog/index.ts} +100 -93
  83. package/apps/bridge/src/mesh/catalog/manifest.ts +20 -0
  84. package/apps/bridge/src/mesh/catalog/models.ts +47 -0
  85. package/apps/bridge/src/mesh/catalog/project/capabilities.ts +56 -0
  86. package/apps/bridge/src/mesh/catalog/project/configured-mcp.ts +80 -0
  87. package/apps/bridge/src/mesh/catalog/project/observations.ts +92 -0
  88. package/apps/bridge/src/mesh/catalog/project/requests.ts +72 -0
  89. package/apps/bridge/src/mesh/context/delta.ts +19 -0
  90. package/apps/bridge/src/mesh/context/env.ts +149 -0
  91. package/apps/bridge/src/mesh/context/packet.ts +54 -0
  92. package/apps/bridge/src/mesh/context/project.ts +155 -0
  93. package/apps/bridge/src/mesh/{prompt-context.ts → context/prompt.ts} +8 -8
  94. package/apps/bridge/src/mesh/delivery/affected-recipients.ts +48 -0
  95. package/apps/bridge/src/mesh/delivery/broadcast-ledger.ts +47 -0
  96. package/apps/bridge/src/mesh/delivery/index.ts +49 -0
  97. package/apps/bridge/src/mesh/{run-digest.ts → delivery/run-digest.ts} +19 -18
  98. package/apps/bridge/src/mesh/handoff/delegation-loop.ts +46 -0
  99. package/apps/bridge/src/mesh/{handoff.ts → handoff/index.ts} +1 -1
  100. package/apps/bridge/src/mesh/handoff/invite-source.ts +25 -0
  101. package/apps/bridge/src/mesh/{other-side.ts → handoff/other-side.ts} +72 -19
  102. package/apps/bridge/src/mesh/{call-scope.ts → identity/call-scope.ts} +1 -1
  103. package/apps/bridge/src/mesh/{computer-identity.ts → identity/computer.ts} +1 -1
  104. package/apps/bridge/src/mesh/identity/device-alias.ts +37 -0
  105. package/apps/bridge/src/mesh/{endpoint.ts → identity/endpoint.ts} +14 -7
  106. package/apps/bridge/src/mesh/{local.ts → local-remote/local.ts} +2 -2
  107. package/apps/bridge/src/mesh/{remote.ts → local-remote/remote.ts} +1 -1
  108. package/apps/bridge/src/mesh/{observed-claims.ts → observed/claims.ts} +12 -7
  109. package/apps/bridge/src/mesh/{integration-map.ts → observed/integration-map.ts} +4 -10
  110. package/apps/bridge/src/mesh/restrictions/check.ts +73 -0
  111. package/apps/bridge/src/mesh/restrictions/cli.ts +40 -0
  112. package/apps/bridge/src/mesh/restrictions/evaluate.ts +100 -0
  113. package/apps/bridge/src/mesh/restrictions/index.ts +19 -0
  114. package/apps/bridge/src/mesh/restrictions/store.ts +98 -0
  115. package/apps/bridge/src/mesh/restrictions/write-gate.ts +63 -0
  116. package/apps/bridge/src/mesh/runtime/capabilities/capability-inventory.ts +9 -0
  117. package/apps/bridge/src/mesh/runtime/capabilities/execution-capabilities.ts +34 -0
  118. package/apps/bridge/src/mesh/runtime/capabilities/index.ts +2 -0
  119. package/apps/bridge/src/mesh/runtime/capabilities/session-discovery.ts +12 -0
  120. package/apps/bridge/src/mesh/{runtime-dependencies.ts → runtime/dependencies.ts} +6 -4
  121. package/apps/bridge/src/mesh/runtime/execution-policy.ts +127 -0
  122. package/apps/bridge/src/mesh/{execution-sweep.ts → runtime/execution-sweep.ts} +1 -1
  123. package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +31 -0
  124. package/apps/bridge/src/mesh/runtime/handoff.ts +204 -0
  125. package/apps/bridge/src/mesh/runtime/index.ts +334 -0
  126. package/apps/bridge/src/mesh/{worktree.ts → runtime/worktree.ts} +9 -8
  127. package/apps/bridge/src/mesh/{map.ts → snapshot/map.ts} +57 -24
  128. package/apps/bridge/src/mesh/snapshot/merge.ts +51 -0
  129. package/apps/bridge/src/mesh/{scoped-view.ts → snapshot/scoped-view.ts} +36 -11
  130. package/apps/bridge/src/mesh/snapshot/window.ts +87 -0
  131. package/apps/bridge/src/mesh/store/base.ts +375 -0
  132. package/apps/bridge/src/mesh/store/index.ts +389 -0
  133. package/apps/bridge/src/mesh/store/rejoin.ts +170 -0
  134. package/apps/bridge/src/mesh/store/state.ts +165 -0
  135. package/apps/bridge/src/mesh/{store-support.ts → store/support.ts} +25 -5
  136. package/apps/bridge/src/mesh/{store-sync.ts → store/sync.ts} +8 -6
  137. package/apps/bridge/src/mesh/{attention.ts → tasks/attention.ts} +1 -1
  138. package/apps/bridge/src/mesh/tasks/create-task.ts +84 -0
  139. package/apps/bridge/src/mesh/tasks/queue.ts +54 -0
  140. package/apps/bridge/src/mesh/{readiness.ts → tasks/readiness.ts} +1 -1
  141. package/apps/bridge/src/mesh/{task-state.ts → tasks/state.ts} +2 -2
  142. package/apps/bridge/src/monitor/graph-refresh.ts +17 -0
  143. package/apps/bridge/src/monitor/handlers/catalog.ts +63 -0
  144. package/apps/bridge/src/monitor/handlers/inbound.ts +232 -0
  145. package/apps/bridge/src/monitor/handlers/receipts.ts +77 -0
  146. package/apps/bridge/src/monitor/handlers/session-commands.ts +165 -0
  147. package/apps/bridge/src/monitor/handlers/task-create.ts +109 -0
  148. package/apps/bridge/src/monitor/handlers/user-message.ts +156 -0
  149. package/apps/bridge/src/monitor/history/pages.ts +60 -0
  150. package/apps/bridge/src/monitor/index.ts +21 -0
  151. package/apps/bridge/src/{monitor-heartbeat.ts → monitor/support/heartbeat.ts} +19 -1
  152. package/apps/bridge/src/{monitor-leadership.ts → monitor/support/leadership.ts} +59 -47
  153. package/apps/bridge/src/{monitor-publish-loop.ts → monitor/support/publish-loop.ts} +3 -1
  154. package/apps/bridge/src/monitor/support/runtime.ts +305 -0
  155. package/apps/bridge/src/{monitor-session-activity.ts → monitor/support/session-activity.ts} +2 -2
  156. package/apps/bridge/src/pairing/controllers.ts +62 -0
  157. package/apps/bridge/src/pairing/index.ts +264 -0
  158. package/apps/bridge/src/pairing/presence.ts +57 -0
  159. package/apps/bridge/src/{pair-uri-file.ts → pairing/uri-file.ts} +1 -1
  160. package/apps/bridge/src/policy/capability-fingerprint.ts +28 -5
  161. package/apps/bridge/src/policy/codex-project-ask.ts +1 -1
  162. package/apps/bridge/src/policy/decision-log.ts +24 -2
  163. package/apps/bridge/src/policy/effective-action.ts +69 -17
  164. package/apps/bridge/src/policy/governed-projects.ts +1 -1
  165. package/apps/bridge/src/{policy.ts → policy/index.ts} +13 -1
  166. package/apps/bridge/src/project-policy/apply.ts +236 -0
  167. package/apps/bridge/src/project-policy/mapping.ts +2 -2
  168. package/apps/bridge/src/project-policy/runtime.ts +17 -227
  169. package/apps/bridge/src/project-policy/types.ts +46 -0
  170. package/apps/bridge/src/{claude-bin.ts → providers/claude-bin.ts} +1 -1
  171. package/apps/bridge/src/providers/codex-bin.ts +33 -0
  172. package/apps/bridge/src/{codex-control.ts → providers/codex-control.ts} +2 -3
  173. package/apps/bridge/src/{cursor-mcp-policy.ts → providers/cursor-mcp-policy.ts} +2 -15
  174. package/apps/bridge/src/{reply.ts → reply/index.ts} +48 -25
  175. package/apps/bridge/src/reply/{attachments.ts → payload/attachments.ts} +2 -2
  176. package/apps/bridge/src/reply/{routing.ts → payload/routing.ts} +1 -1
  177. package/apps/bridge/src/reply/{process.ts → process/index.ts} +30 -8
  178. package/apps/bridge/src/reply/{provider-headless.ts → process/provider-headless.ts} +23 -10
  179. package/apps/bridge/src/sessions/cursor/activity.ts +22 -22
  180. package/apps/bridge/src/sessions/cursor/catalog.ts +111 -28
  181. package/apps/bridge/src/sessions/cursor/index.ts +12 -0
  182. package/apps/bridge/src/sessions/cursor/scan.ts +119 -31
  183. package/apps/bridge/src/sessions/cursor/transcripts.ts +21 -20
  184. package/apps/bridge/src/{sessions.ts → sessions/index.ts} +68 -37
  185. package/apps/bridge/src/sessions/scan/claude/activity.ts +276 -0
  186. package/apps/bridge/src/sessions/scan/claude/index.ts +3 -0
  187. package/apps/bridge/src/sessions/scan/claude/parse.ts +165 -0
  188. package/apps/bridge/src/sessions/scan/claude/scan.ts +36 -0
  189. package/apps/bridge/src/sessions/scan/claude/shared.ts +190 -0
  190. package/apps/bridge/src/sessions/scan/codex/activity.ts +347 -0
  191. package/apps/bridge/src/sessions/scan/codex/assemble.ts +153 -0
  192. package/apps/bridge/src/sessions/scan/codex/index.ts +3 -0
  193. package/apps/bridge/src/sessions/scan/codex/parse.ts +174 -0
  194. package/apps/bridge/src/sessions/scan/codex/scan.ts +33 -0
  195. package/apps/bridge/src/sessions/scan/codex/shared.ts +201 -0
  196. package/apps/bridge/src/sessions/{grok.ts → scan/grok.ts} +13 -13
  197. package/apps/bridge/src/sessions/{activity-helpers.ts → support/activity-helpers.ts} +44 -17
  198. package/apps/bridge/src/sessions/{capability-totals.ts → support/capability-totals.ts} +0 -0
  199. package/apps/bridge/src/sessions/{child-threads.ts → support/child-threads.ts} +7 -5
  200. package/apps/bridge/src/sessions/{common.ts → support/common.ts} +9 -7
  201. package/apps/bridge/src/sessions/{telemetry.ts → telemetry/index.ts} +9 -9
  202. package/apps/bridge/src/tools/updater.ts +9 -5
  203. package/apps/mcp/src/bin/authorize.ts +5 -6
  204. package/apps/mcp/src/bin/serve.ts +4 -5
  205. package/apps/mcp/src/bin/status.ts +2 -2
  206. package/apps/mcp/src/connection-center/README.md +3 -2
  207. package/apps/mcp/src/connection-center/enrollment.ts +44 -0
  208. package/apps/mcp/src/connection-center/state.ts +32 -9
  209. package/apps/mcp/src/connection-center/view.js +42 -10
  210. package/apps/mcp/src/connection-center/widget.html +5 -4
  211. package/apps/mcp/src/cursor-config.ts +35 -3
  212. package/apps/mcp/src/{create-server.ts → http/create-server.ts} +7 -7
  213. package/apps/mcp/src/http/http-server.ts +106 -0
  214. package/apps/mcp/src/http/mcp-handler.ts +62 -0
  215. package/apps/mcp/src/http/oauth-routes.ts +109 -0
  216. package/apps/mcp/src/http-service/common.ts +4 -0
  217. package/apps/mcp/src/{http-service.ts → http-service/index.ts} +4 -4
  218. package/apps/mcp/src/http-service/installer.ts +4 -4
  219. package/apps/mcp/src/http-service/snapshot.ts +2 -2
  220. package/apps/mcp/src/mcp-tools/README.md +2 -1
  221. package/apps/mcp/src/mcp-tools/{connect.ts → connect/index.ts} +39 -29
  222. package/apps/mcp/src/mcp-tools/{relay.ts → connect/relay.ts} +46 -13
  223. package/apps/mcp/src/mcp-tools/{connection-widget.ts → connect/widget.ts} +3 -3
  224. package/apps/mcp/src/mcp-tools/interaction/answers.ts +39 -0
  225. package/apps/mcp/src/mcp-tools/interaction/index.ts +27 -0
  226. package/apps/mcp/src/mcp-tools/interaction/notify.ts +130 -0
  227. package/apps/mcp/src/mcp-tools/interaction/publish.ts +154 -0
  228. package/apps/mcp/src/mcp-tools/interaction/questions.ts +75 -0
  229. package/apps/mcp/src/mcp-tools/{mesh-actions.ts → mesh/actions.ts} +38 -7
  230. package/apps/mcp/src/mcp-tools/mesh/current.ts +100 -0
  231. package/apps/mcp/src/mcp-tools/{mesh-relay.ts → mesh/relay.ts} +4 -4
  232. package/apps/mcp/src/mcp-tools/mesh/resource.ts +68 -0
  233. package/apps/mcp/src/mcp-tools/mesh/session-env.ts +13 -0
  234. package/apps/mcp/src/mcp-tools/mesh/skills/resource.ts +52 -0
  235. package/apps/mcp/src/mcp-tools/{mesh-tools.ts → mesh/tools.ts} +1 -1
  236. package/apps/mcp/src/{mesh-server.ts → mesh/mesh-server.ts} +1 -1
  237. package/apps/mcp/src/oauth/README.md +1 -1
  238. package/apps/mcp/src/oauth/consent/after-consent.ts +31 -0
  239. package/apps/mcp/src/oauth/{pairing-view.ts → consent/pairing-view.ts} +74 -19
  240. package/apps/mcp/src/oauth/consent/pending.ts +64 -0
  241. package/apps/mcp/src/oauth/consent/website-session.ts +180 -0
  242. package/apps/mcp/src/oauth/session/connect-snapshot.ts +91 -0
  243. package/apps/mcp/src/oauth/{loopback-origin.ts → session/loopback-origin.ts} +7 -1
  244. package/apps/mcp/src/oauth/session/mailbox-claim.ts +84 -0
  245. package/apps/mcp/src/oauth/{store.ts → session/store.ts} +4 -21
  246. package/apps/mcp/src/oauth-provider.ts +82 -21
  247. package/apps/mcp/src/server.ts +5 -3
  248. package/apps/mcp/src/{package-version.ts → status/package-version.ts} +1 -1
  249. package/apps/mcp/src/status/pairing-status.ts +71 -0
  250. package/apps/mcp/src/{provider-status.ts → status/provider-status.ts} +6 -9
  251. package/bin/granttap-mcp.mjs +26 -12
  252. package/cursor-plugin/.cursor-plugin/plugin.json +3 -3
  253. package/cursor-plugin/README.md +40 -37
  254. package/cursor-plugin/commands/connect.md +8 -14
  255. package/cursor-plugin/mcp.json +5 -2
  256. package/cursor-plugin/skills/connect/SKILL.md +19 -75
  257. package/cursor-plugin/stdio-bootstrap.cjs +60 -0
  258. package/docs/cursor-authorize.md +14 -8
  259. package/docs/images/iphone-task-route.png +0 -0
  260. package/package.json +5 -4
  261. package/packages/core/crypto.ts +35 -0
  262. package/packages/core/relay-client-types.ts +4 -0
  263. package/packages/core/relay-client.ts +34 -7
  264. package/packages/core/replay-store.ts +34 -0
  265. package/packages/protocol/messages/capabilities.ts +1 -0
  266. package/packages/protocol/messages/{interaction.ts → interaction/index.ts} +39 -3
  267. package/packages/protocol/messages/{mesh.ts → mesh/events.ts} +2 -159
  268. package/packages/protocol/messages/mesh/index.ts +5 -0
  269. package/packages/protocol/messages/mesh/invocations.ts +81 -0
  270. package/packages/protocol/messages/mesh/knowledge/index.ts +19 -0
  271. package/packages/protocol/messages/mesh/snapshot.ts +317 -0
  272. package/packages/protocol/messages/mesh/tasks.ts +117 -0
  273. package/packages/protocol/messages/pairing-join.ts +36 -0
  274. package/packages/protocol/messages/project-policy.ts +112 -1
  275. package/packages/protocol/messages/{sessions.ts → sessions/index.ts} +47 -3
  276. package/packages/protocol/schema.ts +17 -0
  277. package/apps/bridge/src/approval-state.ts +0 -717
  278. package/apps/bridge/src/capabilities.ts +0 -27
  279. package/apps/bridge/src/engine/engine-projects.ts +0 -68
  280. package/apps/bridge/src/engine/engine-protocol.ts +0 -265
  281. package/apps/bridge/src/install.ts +0 -817
  282. package/apps/bridge/src/mesh/runtime-handoff.ts +0 -184
  283. package/apps/bridge/src/mesh/runtime.ts +0 -201
  284. package/apps/bridge/src/mesh/snapshot-merge.ts +0 -32
  285. package/apps/bridge/src/mesh/store-state.ts +0 -292
  286. package/apps/bridge/src/mesh/store.ts +0 -666
  287. package/apps/bridge/src/monitor.ts +0 -786
  288. package/apps/bridge/src/pairing.ts +0 -136
  289. package/apps/bridge/src/sessions/claude.ts +0 -613
  290. package/apps/bridge/src/sessions/codex.ts +0 -776
  291. package/apps/bridge/src/sessions/cursor.ts +0 -9
  292. package/apps/mcp/src/http-server.ts +0 -249
  293. package/apps/mcp/src/mcp-tools/interaction.ts +0 -407
  294. package/apps/mcp/src/mcp-tools/mesh-resource.ts +0 -134
  295. package/apps/mcp/src/pairing-status.ts +0 -25
  296. /package/apps/bridge/src/config/{paths.ts → runtime/paths.ts} +0 -0
  297. /package/apps/bridge/src/engine/{engine-policy-protocol.ts → protocol/engine-policy-protocol.ts} +0 -0
  298. /package/apps/bridge/src/engine/{engine-policy-types.ts → protocol/engine-policy-types.ts} +0 -0
  299. /package/apps/bridge/src/engine/{engine-health.ts → runtime/engine-health.ts} +0 -0
  300. /package/apps/bridge/src/{self-protection.ts → host/self-protection.ts} +0 -0
  301. /package/apps/bridge/src/machine-load/{disk-usage.ts → host/disk-usage.ts} +0 -0
  302. /package/apps/bridge/src/machine-load/{scan-cost.ts → mcp/scan-cost.ts} +0 -0
  303. /package/apps/bridge/src/mesh/{identity.ts → identity/index.ts} +0 -0
  304. /package/apps/bridge/src/mesh/{observed-writes.ts → observed/writes.ts} +0 -0
  305. /package/apps/bridge/src/{monitor-single-flight.ts → monitor/support/single-flight.ts} +0 -0
  306. /package/apps/bridge/src/reply/{types.ts → payload/types.ts} +0 -0
  307. /package/apps/bridge/src/reply/{cursor-agent-bin.ts → process/cursor-agent-bin.ts} +0 -0
  308. /package/apps/bridge/src/sessions/{codex-head.ts → support/codex-head.ts} +0 -0
  309. /package/apps/bridge/src/sessions/{edit-stats.ts → support/edit-stats.ts} +0 -0
  310. /package/apps/mcp/src/{mesh-stdio.ts → mesh/mesh-stdio.ts} +0 -0
  311. /package/packages/protocol/messages/{mesh-endpoint.ts → mesh/endpoint.ts} +0 -0
package/README.md CHANGED
@@ -54,15 +54,38 @@ and completion events through the existing `notify` tool, and read state from a
54
54
  Mesh resource. Full transcripts and hidden reasoning are never mesh payloads.
55
55
  Claims have TTLs; a colliding claim is rejected before it is recorded so agents
56
56
  can choose different work or contact the owner before escalating to Needs You.
57
+ Observed edit-tool requests are intent claims, not confirmed writes. Resource
58
+ identity includes the repository; equal relative paths from separate repositories
59
+ do not conflict, and another checkout is a coordination warning. The path
60
+ matcher respects segment boundaries, so `src/auth.ts` and `src/auth.ts.bak`
61
+ are distinct. A live repository reading refreshes HEAD after commit or checkout.
62
+
63
+ When a separately distributed, verified GrantTap Engine is enabled on the
64
+ computer, the bridge sends content-free Invocation events before its small Usage
65
+ buffer is trimmed. Claude Code, Codex, and Cursor transcript scanners record
66
+ native call identity, request, reported outcome, resource path, and explicit
67
+ source gaps. The Engine owns the durable journal and bounded cursor history;
68
+ the phone requests the latest or older page under the Project E2EE key. A
69
+ reported successful tool call alone does not prove a filesystem change, and
70
+ unavailable Engine history is reported as unavailable.
71
+ The bridge does not yet produce verified filesystem-change events or attach
72
+ revision-bound impact/consumer links to individual Invocations. It leaves that
73
+ evidence unknown instead of treating an edit request or reported success as a
74
+ confirmed content change.
75
+ Claude Code and Codex denials with native call IDs are recorded as separate,
76
+ metadata-only hook events with the applied rule and policy revision; an
77
+ unidentified Cursor decision is not assigned to an invented call. Script and
78
+ `SKILL.md` fingerprints include a content hash when the local file is readable,
79
+ separate from their path identity. Missing content evidence remains unknown.
57
80
 
58
81
  <p align="center">
59
82
  <img src="docs/images/iphone-projects-shared.png" width="200" alt="Projects: one owned by this phone, one shared by another">
60
83
  <img src="docs/images/iphone-project-mesh.png" width="200" alt="A Project: Governance, members and computers, Mesh status, repositories, Tasks">
61
- <img src="docs/images/iphone-task-route.png" width="200" alt="A Task: its executions, its claims, and who else is in its files">
84
+ <img src="docs/images/iphone-task-route.png" width="200" alt="A Task: executions, Runtime history, and resource claims">
62
85
  <img src="docs/images/iphone-handoff.png" width="200" alt="Task handoff: destination, push, and readiness checks">
63
86
  </p>
64
87
 
65
- <p align="center"><em>Projects, one of them shared by another phone · a Project with its Governance, members, computers, repositories, and Tasks · a Task with its executions and claims · a handoff with its readiness checks</em></p>
88
+ <p align="center"><em>Projects, one of them shared by another phone · a Project with its Governance, members, computers, repositories, and Tasks · a Task with executions, Runtime history, and claims · a handoff with its readiness checks</em></p>
66
89
 
67
90
  For Claude Code, Codex, and Cursor, the provider hook runs inside the agent's
68
91
  own session and sees the exact call. GrantTap attributes every `notify` to the
@@ -162,6 +185,14 @@ and from then on that computer takes part in the mesh like any other: its
162
185
  chats are the Project's, its claims are seen by every other computer, and a
163
186
  Task can be handed to it.
164
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
+
165
196
  ### A claim released by the person
166
197
 
167
198
  A claim outlives an agent that crashed or was closed, and the files it names
@@ -223,16 +254,21 @@ grok plugin marketplace add sergii-ziborov/granttap-mcp
223
254
  grok plugin install granttap --trust
224
255
  ```
225
256
 
226
- Import `sergii-ziborov/granttap-mcp` as a private Git plugin in Cursor. The
227
- MIT-licensed plugin is ready for Cursor's public review. Reload Cursor, then open
228
- **Cursor Customize → MCPs → GrantTap → Authenticate**. Complete authorization
229
- and any one-time QR pairing on `https://granttap.com/connect`.
230
-
231
- Open GrantTap from the plugin page or ask `Show my GrantTap pairing QR.` Codex
232
- renders an interactive connection card with pairing status, a one-time QR,
233
- copy-link fallback, and confirmed reconnect controls. The same tools still
234
- return readable MCP content in clients that do not render app UI. Detailed
235
- plugin instructions are in
257
+ For Cursor, use the reviewed **GrantTap** Marketplace listing when it is visible
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
236
272
  [`plugins/granttap/README.md`](plugins/granttap/README.md).
237
273
 
238
274
  For the full background helper and provider hooks, install the CLI and run
@@ -244,8 +280,8 @@ granttap setup
244
280
  ```
245
281
 
246
282
  `granttap setup` detects supported local agents, installs or repairs their
247
- hooks, installs the background helper, configures Cursor's persistent local
248
- 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
249
285
  interactive terminal. It ends with one exact next action.
250
286
 
251
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(