granttap-mcp 0.8.24 → 0.8.30

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 (177) hide show
  1. package/README.md +62 -11
  2. package/THIRD_PARTY_NOTICES.md +7 -0
  3. package/apps/bridge/src/approvals/index.ts +2 -1
  4. package/apps/bridge/src/bin/cli/setup.ts +5 -2
  5. package/apps/bridge/src/bin/hooks/cursor-after-shell.ts +2 -1
  6. package/apps/bridge/src/bin/runtime/monitor.ts +6 -1
  7. package/apps/bridge/src/capabilities/skills.ts +17 -1
  8. package/apps/bridge/src/codex-hook-trust/README.md +34 -0
  9. package/apps/bridge/src/codex-hook-trust/config.ts +79 -0
  10. package/apps/bridge/src/codex-hook-trust/index.ts +59 -0
  11. package/apps/bridge/src/codex-hook-trust/projection.ts +44 -0
  12. package/apps/bridge/src/codex-hook-trust/relay.ts +24 -0
  13. package/apps/bridge/src/codex-hook-trust/rpc.ts +55 -0
  14. package/apps/bridge/src/codex-hook-trust/trust.ts +42 -0
  15. package/apps/bridge/src/cortex/integration.ts +6 -4
  16. package/apps/bridge/src/device-network/README.md +18 -0
  17. package/apps/bridge/src/device-network/directory.ts +39 -0
  18. package/apps/bridge/src/device-network/own-relay.ts +121 -0
  19. package/apps/bridge/src/device-network/settings.ts +52 -0
  20. package/apps/bridge/src/engine/README.md +5 -2
  21. package/apps/bridge/src/engine/protocol/impact/engine-impact-protocol.ts +74 -0
  22. package/apps/bridge/src/engine/protocol/transport/identity-result-parser.ts +26 -0
  23. package/apps/bridge/src/engine/protocol/transport/project-catalog-parser.ts +24 -0
  24. package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +7 -0
  25. package/apps/bridge/src/engine/protocol/transport/result-parser.ts +7 -17
  26. package/apps/bridge/src/engine/runtime/engine-client.ts +3 -0
  27. package/apps/bridge/src/engine/runtime/engine-projects.ts +4 -1
  28. package/apps/bridge/src/engine/runtime/repository-graph-jobs.ts +16 -0
  29. package/apps/bridge/src/host/self-protection.ts +3 -25
  30. package/apps/bridge/src/install/hooks.ts +5 -34
  31. package/apps/bridge/src/install/inspect.ts +12 -78
  32. package/apps/bridge/src/machine-load/README.md +17 -3
  33. package/apps/bridge/src/machine-load/command-process/README.md +13 -0
  34. package/apps/bridge/src/machine-load/command-process/history.ts +205 -0
  35. package/apps/bridge/src/machine-load/host/agent-load-history.ts +14 -11
  36. package/apps/bridge/src/machine-load/host/loop.ts +1 -1
  37. package/apps/bridge/src/machine-load/host/process-ownership.ts +115 -0
  38. package/apps/bridge/src/machine-load/host/process-sampler.ts +23 -177
  39. package/apps/bridge/src/machine-load/host/ps-reader.ts +93 -0
  40. package/apps/bridge/src/machine-load/index.ts +3 -0
  41. package/apps/bridge/src/machine-load/mcp/refresh.ts +1 -5
  42. package/apps/bridge/src/mesh/README.md +9 -0
  43. package/apps/bridge/src/mesh/catalog/README.md +22 -0
  44. package/apps/bridge/src/mesh/catalog/index.ts +15 -13
  45. package/apps/bridge/src/mesh/catalog/models.ts +17 -2
  46. package/apps/bridge/src/mesh/identity/computer.ts +5 -0
  47. package/apps/bridge/src/mesh/repository-details/README.md +25 -0
  48. package/apps/bridge/src/mesh/repository-details/index.ts +61 -0
  49. package/apps/bridge/src/mesh/restrictions/README.md +14 -0
  50. package/apps/bridge/src/mesh/restrictions/evaluate.ts +15 -0
  51. package/apps/bridge/src/mesh/runtime/execution-policy.ts +56 -9
  52. package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +13 -5
  53. package/apps/bridge/src/mesh/runtime/index.ts +54 -69
  54. package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +50 -0
  55. package/apps/bridge/src/mesh/tasks/create-task.ts +10 -4
  56. package/apps/bridge/src/monitor/handlers/catalog.ts +5 -1
  57. package/apps/bridge/src/monitor/handlers/inbound.ts +5 -1
  58. package/apps/bridge/src/monitor/handlers/user-message.ts +19 -10
  59. package/apps/bridge/src/monitor/support/runtime.ts +9 -63
  60. package/apps/bridge/src/monitor/support/session-activity.ts +8 -1
  61. package/apps/bridge/src/pairing/index.ts +7 -4
  62. package/apps/bridge/src/policy/effective-action.ts +33 -3
  63. package/apps/bridge/src/policy/impact/impact-evidence.ts +51 -0
  64. package/apps/bridge/src/policy/index.ts +2 -34
  65. package/apps/bridge/src/policy/shell/README.md +11 -0
  66. package/apps/bridge/src/policy/shell/safe-shell.ts +41 -0
  67. package/apps/bridge/src/project-policy/apply.ts +39 -12
  68. package/apps/bridge/src/project-policy/runtime.ts +16 -13
  69. package/apps/bridge/src/project-policy/types.ts +2 -2
  70. package/apps/bridge/src/provider-model-catalog/README.md +22 -0
  71. package/apps/bridge/src/provider-model-catalog/index.ts +63 -0
  72. package/apps/bridge/src/providers/codex-bin.ts +7 -2
  73. package/apps/bridge/src/sessions/cursor/scan.ts +3 -6
  74. package/apps/bridge/src/sessions/cursor/transcripts.ts +1 -3
  75. package/apps/bridge/src/sessions/index.ts +8 -9
  76. package/apps/bridge/src/sessions/scan/claude/activity.ts +16 -6
  77. package/apps/bridge/src/sessions/scan/claude/message-identity.ts +9 -0
  78. package/apps/bridge/src/sessions/scan/claude/parse.ts +11 -3
  79. package/apps/bridge/src/sessions/scan/claude/repository-observation/index.ts +16 -0
  80. package/apps/bridge/src/sessions/scan/claude/shared.ts +36 -16
  81. package/apps/bridge/src/sessions/scan/codex/activity/rows.ts +111 -0
  82. package/apps/bridge/src/sessions/scan/codex/activity.ts +15 -110
  83. package/apps/bridge/src/sessions/scan/codex/image/index.ts +44 -0
  84. package/apps/bridge/src/sessions/scan/codex/parse.ts +9 -4
  85. package/apps/bridge/src/sessions/scan/codex/repository-observation/index.ts +21 -0
  86. package/apps/bridge/src/sessions/scan/codex/shared.ts +36 -52
  87. package/apps/bridge/src/sessions/scan/codex/turn-state/index.ts +35 -0
  88. package/apps/bridge/src/sessions/support/activity-helpers.ts +8 -3
  89. package/apps/bridge/src/sessions/support/capability-totals.ts +0 -0
  90. package/apps/bridge/src/sessions/support/repository-checkout/index.ts +40 -0
  91. package/apps/bridge/src/sessions/telemetry/README.md +7 -3
  92. package/apps/bridge/src/sessions/telemetry/command-preview.ts +11 -5
  93. package/apps/bridge/src/sessions/telemetry/index.ts +7 -50
  94. package/apps/bridge/src/sessions/telemetry/wire-budget.ts +53 -0
  95. package/apps/bridge/src/transcript-history/README.md +28 -0
  96. package/apps/bridge/src/transcript-history/changes.ts +93 -0
  97. package/apps/bridge/src/transcript-history/images.ts +29 -0
  98. package/apps/bridge/src/transcript-history/index.ts +82 -0
  99. package/apps/bridge/src/transcript-history/native-lines.ts +37 -0
  100. package/apps/bridge/src/transcript-history/tool-details.ts +34 -0
  101. package/apps/bridge/src/transcript-history/turn-changes.ts +71 -0
  102. package/apps/mcp/src/bin/serve.ts +8 -3
  103. package/apps/mcp/src/bin/status.ts +2 -0
  104. package/apps/mcp/src/connection-center/README.md +5 -4
  105. package/apps/mcp/src/connection-center/state.ts +1 -1
  106. package/apps/mcp/src/desktop/README.md +48 -0
  107. package/apps/mcp/src/desktop/delivery/README.md +16 -0
  108. package/apps/mcp/src/desktop/delivery/attachments.ts +37 -0
  109. package/apps/mcp/src/desktop/delivery/mesh-create.ts +45 -0
  110. package/apps/mcp/src/desktop/delivery/task-create.ts +98 -0
  111. package/apps/mcp/src/desktop/delivery/task-send.ts +63 -0
  112. package/apps/mcp/src/desktop/engine-bridge.ts +146 -0
  113. package/apps/mcp/src/desktop/image/artifacts.ts +97 -0
  114. package/apps/mcp/src/desktop/image/index.ts +29 -0
  115. package/apps/mcp/src/desktop/invocation/index.ts +39 -0
  116. package/apps/mcp/src/desktop/live-catalog/README.md +10 -0
  117. package/apps/mcp/src/desktop/live-catalog/index.ts +40 -0
  118. package/apps/mcp/src/desktop/load/index.ts +35 -0
  119. package/apps/mcp/src/desktop/mesh-project.ts +123 -0
  120. package/apps/mcp/src/desktop/models/README.md +12 -0
  121. package/apps/mcp/src/desktop/models/index.ts +13 -0
  122. package/apps/mcp/src/desktop/native-access/README.md +19 -0
  123. package/apps/mcp/src/desktop/native-access/access.ts +67 -0
  124. package/apps/mcp/src/desktop/native-access/invoke.ts +34 -0
  125. package/apps/mcp/src/desktop/native-access/routes.ts +57 -0
  126. package/apps/mcp/src/desktop/network/README.md +17 -0
  127. package/apps/mcp/src/desktop/network/index.ts +28 -0
  128. package/apps/mcp/src/desktop/operations/index.ts +86 -0
  129. package/apps/mcp/src/desktop/pairing/README.md +22 -0
  130. package/apps/mcp/src/desktop/pairing/index.ts +83 -0
  131. package/apps/mcp/src/desktop/policy/README.md +17 -0
  132. package/apps/mcp/src/desktop/policy/auto-accept.ts +25 -0
  133. package/apps/mcp/src/desktop/policy/index.ts +42 -0
  134. package/apps/mcp/src/desktop/policy/scope.ts +20 -0
  135. package/apps/mcp/src/desktop/process/bridge-child.ts +20 -0
  136. package/apps/mcp/src/desktop/process/bridge-process.ts +61 -0
  137. package/apps/mcp/src/desktop/project-catalog.ts +30 -0
  138. package/apps/mcp/src/desktop/projection/installed-skills.ts +38 -0
  139. package/apps/mcp/src/desktop/projection/selected-mesh.ts +33 -0
  140. package/apps/mcp/src/desktop/projection/session-usage.ts +18 -0
  141. package/apps/mcp/src/desktop/projection/usage.ts +20 -0
  142. package/apps/mcp/src/desktop/provider-hooks/README.md +15 -0
  143. package/apps/mcp/src/desktop/provider-hooks/index.ts +12 -0
  144. package/apps/mcp/src/desktop/provider-storage/README.md +29 -0
  145. package/apps/mcp/src/desktop/provider-storage/fingerprint.ts +19 -0
  146. package/apps/mcp/src/desktop/provider-storage/index.ts +117 -0
  147. package/apps/mcp/src/desktop/provider-storage/scanner.ts +31 -0
  148. package/apps/mcp/src/desktop/task-activity-runner.ts +178 -0
  149. package/apps/mcp/src/desktop/task-activity-worker.ts +60 -0
  150. package/apps/mcp/src/desktop/task-activity.ts +84 -0
  151. package/apps/mcp/src/http/http-server.ts +27 -4
  152. package/apps/mcp/src/http-service/README.md +10 -0
  153. package/apps/mcp/src/http-service/installer.ts +10 -2
  154. package/apps/mcp/src/mcp-tools/connect/relay.ts +6 -1
  155. package/apps/mcp/src/oauth/README.md +1 -1
  156. package/apps/mcp/src/oauth/consent/website-session.ts +6 -4
  157. package/apps/mcp/src/oauth/session/connect-snapshot.ts +4 -1
  158. package/apps/mcp/src/oauth-provider.ts +14 -8
  159. package/apps/mcp/src/status/desktop-status.ts +32 -0
  160. package/apps/mcp/src/status/provider-status.ts +18 -4
  161. package/cursor-plugin/README.md +1 -1
  162. package/cursor-plugin/mcp.json +1 -1
  163. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  164. package/docs/images/iphone-company-accounts.png +0 -0
  165. package/docs/images/iphone-company-device-invite.png +0 -0
  166. package/docs/images/iphone-company-repositories.png +0 -0
  167. package/package.json +1 -1
  168. package/packages/core/relay-client-types.ts +2 -0
  169. package/packages/protocol/messages/capabilities.ts +12 -0
  170. package/packages/protocol/messages/interaction/index.ts +2 -0
  171. package/packages/protocol/messages/mesh/index.ts +1 -0
  172. package/packages/protocol/messages/mesh/repository-details/index.ts +24 -0
  173. package/packages/protocol/messages/mesh/snapshot.ts +10 -0
  174. package/packages/protocol/messages/provider-hooks/README.md +20 -0
  175. package/packages/protocol/messages/provider-hooks/index.ts +53 -0
  176. package/packages/protocol/messages/sessions/index.ts +6 -12
  177. package/packages/protocol/schema.ts +4 -0
package/README.md CHANGED
@@ -4,24 +4,26 @@
4
4
  [![CI](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
 
7
- GrantTap is a Personal live control center for local coding agents.
7
+ GrantTap is a distributed Project control system for local coding agents.
8
8
 
9
9
  > See what your coding agents are doing. Step in when they need you.
10
10
 
11
11
  This repository is the canonical machine runtime: CLI, MCP server, provider
12
- hooks, local adapters, and TypeScript wire schemas. Agents and provider
13
- credentials stay on your computer. Native iPhone and Apple Watch traffic is
14
- end-to-end encrypted.
12
+ hooks, local adapters, and TypeScript wire schemas. It connects the Mac,
13
+ iPhone, iPad, and Apple Watch product surfaces to local agents, Project Mesh,
14
+ Governance, auto-accept, and Engine-backed evidence. Agents and provider
15
+ credentials stay on your computer. Native controller traffic is end-to-end
16
+ encrypted.
15
17
 
16
- The MCP runtime, plugins, and CLI are open source under the MIT License. The
17
- iPhone and Apple Watch app is a separate, proprietary product; use of GrantTap's
18
- hosted service is governed by its own terms.
18
+ The MCP runtime, plugins, and CLI are open source under the MIT License.
19
+ The Apple apps, Engine, relay, and website are separately licensed commercial
20
+ products or source. Their licenses do not change this repository's MIT grant.
19
21
 
20
22
  [Website](https://granttap.com) · [npm](https://www.npmjs.com/package/granttap-mcp) ·
21
23
  [Security model](SECURITY.md) ·
22
24
  [Relay source](https://github.com/sergii-ziborov/granttap-relay)
23
25
 
24
- ## iPhone and Apple Watch
26
+ ## Mac, iPhone, iPad, and Apple Watch
25
27
 
26
28
  <p align="center">
27
29
  <img src="docs/images/iphone-command-center.png" width="230" alt="GrantTap Now with Needs You and at-risk tasks">
@@ -43,6 +45,22 @@ hosted service is governed by its own terms.
43
45
  GrantTap reports the depth each provider actually exposes. Visibility does not
44
46
  imply deterministic remote blocking or full mobile continuation.
45
47
 
48
+ The Mac app checks this runtime's local health endpoint without account login
49
+ or QR scanning. A Mac App Store sandbox build then asks for explicit local
50
+ access using the runtime's bounded PKCE consent flow; development builds retain
51
+ private Unix IPC. Mac Settings can configure encrypted address discovery or
52
+ install/start/stop the pinned personal relay. Runtime code remains outside the
53
+ Apple sandbox. Mac purchase terms, optional Personal subscriptions and required
54
+ TLS/VPN reachability are documented in the Apple client repository.
55
+ The local HTTP MCP OAuth flow can be approved with a fresh GrantTap account
56
+ passkey on Mac. This authorizes the selected coding app to use this computer's
57
+ MCP endpoint; it does not pair a phone. Direct QR pairing remains available
58
+ without a GrantTap account.
59
+ The iPhone and iPad app provides the mobile Project controller;
60
+ Apple Watch carries the compact attention and reply flow through iPhone.
61
+ The machine runtime and separately distributed Engine supply Project records,
62
+ policy coverage, Invocation history, and the distributed evidence graph.
63
+
46
64
  Session catalogs report `lastMessageAt` only when a provider transcript has a
47
65
  timestamped user or assistant message. `lastActivityAt` remains the separate
48
66
  runtime activity timestamp and can include metadata or file updates. Clients
@@ -187,6 +205,17 @@ of a topic it produces, the caller of an API it changes), and the scoped
187
205
 
188
206
  A Project is shared from the phone that owns it, and that phone stays the
189
207
  hub: nothing a member does reaches a computer without passing through it.
208
+ The owner manages **company accounts** and repository grants separately from
209
+ Project Mesh membership. An account can cover selected repository IDs or all
210
+ company repositories; it does not join a Mesh merely because it has repository
211
+ access. A phone or tablet may pair to the account before any computer or
212
+ Project exists, with zero Project permissions. The owner then grants selected
213
+ Project IDs and a role to that device. Several devices may use the same account with different
214
+ Mesh grants. Before the phone forwards a complete Project snapshot, its key,
215
+ chats, or actions, the account must cover every repository ID in that Project.
216
+ Linked sub-Mesh Projects still require separate selection. These owner-phone
217
+ grants do not edit Git provider ACLs or give the MCP a company login.
218
+
190
219
  *Invite a person* makes a one-time code, good for fifteen minutes; the other
191
220
  phone scans it under *Projects → Join a Project*, and the Project arrives
192
221
  there with the role the owner chose — Viewer, Member, or Admin — and the four
@@ -198,13 +227,16 @@ a role takes effect at once; removing a member stops the forwarding at once,
198
227
  though nothing already seen can be recalled.
199
228
 
200
229
  <p align="center">
230
+ <img src="docs/images/iphone-company-accounts.png" width="200" alt="Company accounts separate from Mesh device links">
231
+ <img src="docs/images/iphone-company-repositories.png" width="200" alt="Selected repository IDs for one company account">
232
+ <img src="docs/images/iphone-company-device-invite.png" width="200" alt="Controller device code before any Mesh Project grant">
201
233
  <img src="docs/images/iphone-members.png" width="200" alt="Members and computers of a Project, seen by its owner">
202
234
  <img src="docs/images/iphone-invite.png" width="200" alt="Invite a person: name, role, and the four answers under it">
203
235
  <img src="docs/images/iphone-join-project.png" width="200" alt="Join a Project from another phone: scan the invite or paste it">
204
236
  <img src="docs/images/iphone-members-shared.png" width="200" alt="A shared Project on the member's phone, with a computer of their own to add">
205
237
  </p>
206
238
 
207
- <p align="center"><em>The owner's members and computers · an invite with its role · joining from another phone · the shared Project as the member sees it, with a computer of their own to add</em></p>
239
+ <p align="center"><em>Company accounts and repository grants · separate Mesh members and device links · joining from another phone</em></p>
208
240
 
209
241
  A member works in the Project with computers of their own. Adding one hands
210
242
  it the Project's mesh key over the pairing the member's phone already trusts,
@@ -214,7 +246,12 @@ Task can be handed to it.
214
246
 
215
247
  A Project can pin new tasks to one confirmed computer. The endpoint id is
216
248
  the stored computer identity, not the display name. Models offered on the
217
- phone come from that host's catalog. If the pinned host is offline, the
249
+ phone come from that host's catalog. Codex picker metadata is read from the
250
+ installed provider's account-scoped model cache, refreshed on each catalog
251
+ cycle. Visible models keep their descriptions, provider ordering and fetch
252
+ timestamp; hidden review/internal models are excluded. Expired data stays
253
+ unknown. Local Mac replies, like relay replies, pass the chosen model to the
254
+ existing native session. If the pinned host is offline, the
218
255
  Project either refuses the create or queues it until a deadline — it does
219
256
  not silently start the task elsewhere. A command issued for a previous
220
257
  instance of the computer (a restore or clone that kept pairing keys) is
@@ -570,6 +607,9 @@ npm run package:allowlist
570
607
  npm run test:coverage # macOS only — see below
571
608
  ```
572
609
 
610
+ Set `GRANTTAP_TEST_CONCURRENCY=1` when running checks alongside heavy builds;
611
+ the default is four test workers, and the same assertions and coverage gates apply.
612
+
573
613
  `npm test` runs everywhere. Both suites run with an isolated `HOME`, so a
574
614
  developer's own Claude/Codex/Cursor data can never inflate a local result: the
575
615
  numbers on this machine and in CI are the same. The coverage contract is
@@ -585,8 +625,19 @@ typecheck, and release checks pass.
585
625
  ## License
586
626
 
587
627
  GrantTap MCP, its CLI, and bundled plugins are distributed under the
588
- [MIT License](LICENSE). The iPhone and Apple Watch app is licensed separately.
628
+ [MIT License](LICENSE). The Mac, iPhone, iPad, and Apple Watch apps are licensed separately.
589
629
  Third-party dependencies retain their own terms; see
590
630
  [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
591
631
 
592
632
  GrantTap is not affiliated with Anthropic, OpenAI, Apple, Anysphere, or xAI.
633
+
634
+ ## Repository facts and execution checkout
635
+
636
+ Version 0.8.29 adds optional encrypted repository Git observations: canonical
637
+ identity, branch/HEAD, dirty state, recent commits, contributors and branch
638
+ totals. Both phone publication and the native Mac reader use the same bounded
639
+ read-only Git source. Missing data is explicit. Claude and Codex can detect
640
+ sustained structured work in a different checkout without interpreting chat
641
+ titles. Task identity and its original coordination/access scope survive.
642
+
643
+ See [repository observations](apps/bridge/src/mesh/repository-details/README.md).
@@ -21,3 +21,10 @@ The unpublished Rust workspace also depends on separately licensed components,
21
21
  including `mcport` (MIT), `blazingly-json` (MIT), `serde` (MIT OR Apache-2.0), and
22
22
  BlindPlane crates (MIT OR Apache-2.0). Binary distributions must retain the
23
23
  complete license texts required by the exact dependency versions they contain.
24
+
25
+ ## Optional SweepLoom integration
26
+
27
+ Provider storage inspection may invoke a separately installed SweepLoom CLI
28
+ ([source](https://github.com/Weavatrix/sweeploom), MPL-2.0). No SweepLoom source or
29
+ binary is included in this npm package. Its license remains independent; the
30
+ GrantTap MIT license does not apply to SweepLoom.
@@ -20,6 +20,7 @@ import {
20
20
  } from "./state";
21
21
  import { primeSessionKeys, sendSessionPayload } from "../host/session-keys";
22
22
  import { controllerPeerGate } from "../pairing/controllers";
23
+ import { applyNetworkRoute } from "../device-network/settings";
23
24
 
24
25
  /**
25
26
  * Nobody answered — relay unreachable, or the phone never responded before the
@@ -45,7 +46,7 @@ export async function requestApproval(
45
46
  opts: RequestApprovalOpts = {},
46
47
  ): Promise<ApprovalDecision> {
47
48
  const timeoutMs = opts.timeoutMs ?? 60_000;
48
- const client = opts.client ?? new RelayClient(cfg, { peerAllowed: controllerPeerGate(cfg) });
49
+ const client = opts.client ?? new RelayClient(applyNetworkRoute(cfg), { peerAllowed: controllerPeerGate(cfg) });
49
50
  const ownsClient = !opts.client;
50
51
  let cancelDecisionWait = () => {};
51
52
  const registration = registerPendingApproval(req);
@@ -1,3 +1,4 @@
1
+ import { refreshCodexHooks, codexHooksReady } from "../../codex-hook-trust";
1
2
  import QRCode from "qrcode";
2
3
  import {
3
4
  CODEX_TRUST_INSTRUCTION,
@@ -75,6 +76,7 @@ async function main(): Promise<void> {
75
76
  cursorConfig = removeCursorUserGrantTap();
76
77
  }
77
78
  }
79
+ const codexTrust = await refreshCodexHooks(true);
78
80
  const pairingResult = await pairIfNeeded();
79
81
  const paired = isMachineConfigured();
80
82
  const installed = new Set(before.agents.filter((item) => item.installed).map((item) => item.agent));
@@ -93,7 +95,7 @@ async function main(): Promise<void> {
93
95
  ? "Needs attention" : "Authorize in Claude Code" : "Not installed"}`,
94
96
  `Codex ${installed.has("codex")
95
97
  ? codexHook.status === "manual" || httpService?.status === "manual"
96
- ? "Needs attention" : !paired ? "Needs connection" : "Authorize in Codex; review hooks"
98
+ ? "Needs attention" : !paired ? "Needs connection" : codexHooksReady(codexTrust.hooks) ? "Hooks trusted and enabled" : "Authorize in Codex; review hooks"
97
99
  : "Not installed"}`,
98
100
  `Cursor ${before.cursor.installed ? `Beta · ${cursorReady ? "Use GrantTap plugin" : "Needs repair"}` : "Not installed"}`,
99
101
  `Grok Build ${installed.has("grok")
@@ -104,7 +106,8 @@ async function main(): Promise<void> {
104
106
  `Project Governance ${engine ? "Ready" : "No engine found"}`,
105
107
  "",
106
108
  paired && installed.has("codex")
107
- ? `Next: authorize GrantTap in Codex, then ${CODEX_TRUST_INSTRUCTION}`
109
+ ? codexHooksReady(codexTrust.hooks) ? "Codex confirmed both GrantTap hooks are trusted and enabled."
110
+ : `Next: review GrantTap hooks in GrantTap Settings → This Mac → Codex hooks, or ${CODEX_TRUST_INSTRUCTION}`
108
111
  : paired && before.cursor.installed
109
112
  ? "Next: keep the GrantTap plugin. Pair and change settings in the GrantTap app, not Cursor MCP settings."
110
113
  : !paired
@@ -10,6 +10,7 @@ import {
10
10
  import { isProviderEnabled, loadConfig, machineConfigPath } from "../../config";
11
11
  import { cursorRootSessionId } from "../../sessions/cursor";
12
12
  import { controllerPeerGate } from "../../pairing/controllers";
13
+ import { applyNetworkRoute } from "../../device-network/settings";
13
14
 
14
15
  const SHELL_TOOLS = new Set([
15
16
  "bash",
@@ -77,7 +78,7 @@ async function main(): Promise<void> {
77
78
  if (cancelled.length > 0) {
78
79
  try {
79
80
  const config = loadConfig(machineConfigPath());
80
- const client = new RelayClient(config, { peerAllowed: controllerPeerGate(config) });
81
+ const client = new RelayClient(applyNetworkRoute(config), { peerAllowed: controllerPeerGate(config) });
81
82
  await client.connect();
82
83
  try {
83
84
  for (const { request, accepted } of cancelled) {
@@ -7,20 +7,24 @@ import { meshSnapshots } from "../../mesh/runtime";
7
7
  import { recoverLocalGraphBindings } from "../../mesh/runtime/graph/binding-sync";
8
8
  import { startSessionMonitor } from "../../monitor";
9
9
  import { controllerPeerGate } from "../../pairing/controllers";
10
+ import { applyNetworkRoute } from "../../device-network/settings";
11
+ import { startEndpointPublisher } from "../../device-network/directory";
10
12
 
11
13
  let client: RelayClient;
12
14
  let monitor: ReturnType<typeof startSessionMonitor>;
13
15
  const engine = new EngineSupervisor();
14
16
  let stopping = false;
17
+ let stopEndpointPublisher = () => {};
15
18
 
16
19
  try {
17
- const cfg = loadConfig(machineConfigPath());
20
+ const cfg = applyNetworkRoute(loadConfig(machineConfigPath()));
18
21
  client = new RelayClient(cfg, {
19
22
  autoReconnect: true,
20
23
  replayPath: join(configDir(), `replay-${cfg.room}.json`),
21
24
  peerAllowed: controllerPeerGate(cfg),
22
25
  });
23
26
  monitor = startSessionMonitor(client);
27
+ stopEndpointPublisher = startEndpointPublisher(cfg);
24
28
  } catch (error) {
25
29
  process.stderr.write(
26
30
  `[granttap-mcp] monitor is not paired: ${error instanceof Error ? error.message : String(error)}\n`,
@@ -32,6 +36,7 @@ const stop = (): void => {
32
36
  if (stopping) return;
33
37
  stopping = true;
34
38
  monitor.close();
39
+ stopEndpointPublisher();
35
40
  client.close();
36
41
  void engine.stop().finally(() => process.exit(0));
37
42
  };
@@ -184,6 +184,22 @@ function clipSkillText(value: string | undefined, max: number): string | undefin
184
184
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
185
185
  }
186
186
 
187
+ function skillDescription(header: string): string | undefined {
188
+ const lines = header.split("\n");
189
+ const index = lines.findIndex((line) => /^description:\s*/.test(line));
190
+ if (index < 0) return undefined;
191
+ const scalar = lines[index]!.replace(/^description:\s*/, "").trim();
192
+ if (!/^[>|][+-]?$/.test(scalar)) {
193
+ return clipSkillText(scalar.replace(/^["']|["']$/g, ""), 500);
194
+ }
195
+ const parts: string[] = [];
196
+ for (const line of lines.slice(index + 1)) {
197
+ if (line.trim() && !/^\s/.test(line)) break;
198
+ parts.push(line.trim());
199
+ }
200
+ return clipSkillText(parts.join(scalar.startsWith(">") ? " " : "\n"), 500);
201
+ }
202
+
187
203
  function readSkillFile(path: string): {
188
204
  name: string;
189
205
  description?: string;
@@ -197,7 +213,7 @@ function readSkillFile(path: string): {
197
213
  const header = body.slice(3, end);
198
214
  const name = header.match(/^name:\s*["']?([^\n"']+)["']?\s*$/m)?.[1]?.trim();
199
215
  if (!name) return undefined;
200
- const description = header.match(/^description:\s*["']?([^\n"']+)["']?\s*$/m)?.[1]?.trim();
216
+ const description = skillDescription(header);
201
217
  const version = header.match(/^version:\s*["']?([^\n"']+)["']?\s*$/m)?.[1]?.trim();
202
218
  return {
203
219
  name: name.slice(0, 160),
@@ -0,0 +1,34 @@
1
+ # Codex hook review
2
+
3
+ Public entry point: [`index.ts`](index.ts).
4
+
5
+ GrantTap reads native Codex `hooks/list` metadata to distinguish configuration,
6
+ current-definition trust, and enabled state. Only the two exact user hooks
7
+ installed by this runtime (`PreToolUse` and `PermissionRequest`) can be reviewed.
8
+ Project, plugin, managed, ambiguous, or unrelated commands are never approved.
9
+ Unsupported native APIs report unknown; configured hooks alone do not establish
10
+ readiness or computer enforcement.
11
+
12
+ The monitor and desktop status endpoint share a bounded cache. Computers with no
13
+ GrantTap hook definition do not start Codex. When a native status probe is needed,
14
+ Codex may maintain its own runtime caches; GrantTap does not edit hook configuration
15
+ or trust during inspection. The subprocess starts no threads or turns.
16
+
17
+ A person approves one displayed definition on one computer. `trustCodexHook`
18
+ checks the computer, five-minute request lifetime, native key, and current hash,
19
+ then uses Codex's `config/batchWrite` API to save that hash and enabled state.
20
+ Success requires a fresh native report confirming the same key and hash are
21
+ trusted and enabled. Modified definitions require a new review. No bulk trust,
22
+ sandbox bypass, or approval-policy change is performed.
23
+
24
+ Remote requests require the paired controller and matching room. Results return
25
+ only to that controller. Mac uses the existing private desktop Engine socket.
26
+ Legacy clients retain their configuration status without claiming native trust.
27
+
28
+ Tests live in `tests/`. The native integration test runs only when
29
+ `GRANTTAP_CODEX_TEST_BIN` names a Codex binary and uses a temporary Codex home.
30
+ It never trusts hooks in the user's configuration.
31
+
32
+ ## License
33
+
34
+ This module is distributed under the MIT License. See the repository LICENSE.
@@ -0,0 +1,79 @@
1
+ import { ownedHookCommands } from "./projection";
2
+
3
+ export function codexHookSet(config: string): { permission: boolean; policy: boolean } {
4
+ type Event = "PermissionRequest" | "PreToolUse";
5
+ let event: Event | null = null;
6
+ let matcherAll = false;
7
+ let inCommandHook = false;
8
+ let commandType = false;
9
+ let command: string | null = null;
10
+ let timeout: number | null = null;
11
+ let permission = false;
12
+ let policy = false;
13
+ const finishCommandHook = () => {
14
+ if (!event || !matcherAll || !inCommandHook || !commandType) return;
15
+ if (event === "PermissionRequest"
16
+ && timeout === 120
17
+ && command === ownedHookCommands().permission) {
18
+ permission = true;
19
+ }
20
+ if (event === "PreToolUse"
21
+ && timeout === 30
22
+ && command === ownedHookCommands().policy) {
23
+ policy = true;
24
+ }
25
+ };
26
+ for (const line of config.split(/\r?\n/)) {
27
+ const parent = line.match(/^\s*\[\[hooks\.(PermissionRequest|PreToolUse)\]\]\s*(?:#.*)?$/i);
28
+ if (parent) {
29
+ finishCommandHook();
30
+ event = parent[1]!.toLowerCase() === "permissionrequest"
31
+ ? "PermissionRequest"
32
+ : "PreToolUse";
33
+ matcherAll = false;
34
+ inCommandHook = false;
35
+ commandType = false;
36
+ command = null;
37
+ timeout = null;
38
+ continue;
39
+ }
40
+ const child = line.match(
41
+ /^\s*\[\[hooks\.(PermissionRequest|PreToolUse)\.hooks\]\]\s*(?:#.*)?$/i,
42
+ );
43
+ if (child) {
44
+ finishCommandHook();
45
+ const childEvent: Event = child[1]!.toLowerCase() === "permissionrequest"
46
+ ? "PermissionRequest"
47
+ : "PreToolUse";
48
+ inCommandHook = event === childEvent;
49
+ commandType = false;
50
+ command = null;
51
+ timeout = null;
52
+ continue;
53
+ }
54
+ if (/^\s*\[/.test(line)) {
55
+ finishCommandHook();
56
+ event = null;
57
+ matcherAll = false;
58
+ inCommandHook = false;
59
+ commandType = false;
60
+ command = null;
61
+ timeout = null;
62
+ continue;
63
+ }
64
+ if (event && !inCommandHook) {
65
+ const matcher = line.match(/^\s*matcher\s*=\s*["'](.*)["']\s*(?:#.*)?$/i)?.[1];
66
+ if (matcher != null) matcherAll = matcher.trim() === ".*";
67
+ } else if (inCommandHook) {
68
+ const type = line.match(/^\s*type\s*=\s*["'](.*)["']\s*(?:#.*)?$/i)?.[1];
69
+ if (type != null) commandType = type.trim().toLowerCase() === "command";
70
+ const value = line.match(/^\s*command\s*=\s*["'](.*)["']\s*(?:#.*)?$/i)?.[1];
71
+ if (value != null) command = value;
72
+ const rawTimeout = line.match(/^\s*timeout\s*=\s*(\d+)\s*(?:#.*)?$/i)?.[1];
73
+ if (rawTimeout != null) timeout = Number(rawTimeout);
74
+ }
75
+ }
76
+ finishCommandHook();
77
+ return { permission, policy };
78
+ }
79
+
@@ -0,0 +1,59 @@
1
+ import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { ProviderHook } from "../../../../packages/protocol/messages/provider-hooks";
5
+ import { codexHookRpc, type HookRpc } from "./rpc";
6
+ import { projectCodexHooks } from "./projection";
7
+ import { codexHookSet } from "./config";
8
+
9
+ export { codexHookRpc, type HookRpc, type HookRpcOptions } from "./rpc";
10
+ export { projectCodexHooks, ownedHookCommands, codexPolicyReady, codexHooksReady } from "./projection";
11
+ export { codexHookSet } from "./config";
12
+ export { trustCodexHook } from "./trust";
13
+
14
+ export type CodexHookReport = { hooks: ProviderHook[]; checkedAt: number };
15
+ let report: CodexHookReport | undefined;
16
+ let pending: Promise<CodexHookReport> | undefined;
17
+ let cacheScope = "";
18
+
19
+ function scope(): string {
20
+ return [process.env.GRANTTAP_CODEX_DIR, process.env.NODVOX_CODEX_DIR, process.env.CODEX_HOME,
21
+ process.env.GRANTTAP_CODEX_BIN, homedir()].join("\u001f");
22
+ }
23
+
24
+ function hasUserHook(): boolean {
25
+ const home = process.env.GRANTTAP_CODEX_DIR ?? process.env.NODVOX_CODEX_DIR ??
26
+ process.env.CODEX_HOME ?? join(homedir(), ".codex");
27
+ try {
28
+ const hooks = codexHookSet(readFileSync(join(home, "config.toml"), "utf8"));
29
+ return hooks.policy || hooks.permission;
30
+ } catch { return false; }
31
+ }
32
+
33
+ export async function readCodexHooks(rpc: HookRpc = codexHookRpc()): Promise<CodexHookReport> {
34
+ try {
35
+ return { hooks: projectCodexHooks(await rpc("hooks/list", { cwds: [homedir()] })), checkedAt: Date.now() };
36
+ } catch {
37
+ return { hooks: projectCodexHooks(null), checkedAt: Date.now() };
38
+ }
39
+ }
40
+
41
+ export function cachedCodexHooks(): CodexHookReport | undefined {
42
+ return cacheScope === scope() && report && Date.now() - report.checkedAt <= 45_000 ? report : undefined;
43
+ }
44
+
45
+ /** Bound subprocess work on both the monitor and loopback status poll. */
46
+ export function refreshCodexHooks(force = false): Promise<CodexHookReport> {
47
+ const currentScope = scope();
48
+ if (currentScope !== cacheScope) { report = undefined; cacheScope = currentScope; }
49
+ if (pending) return pending;
50
+ if (!force && report && Date.now() - report.checkedAt < 15_000) return Promise.resolve(report);
51
+ // Do not initialize Codex or its cache on computers with no GrantTap hook definition.
52
+ pending = (hasUserHook() ? readCodexHooks() : Promise.resolve({
53
+ hooks: projectCodexHooks({ data: [{ hooks: [] }] }), checkedAt: Date.now(),
54
+ })).then(value => { if (currentScope === scope()) report = value; return value; })
55
+ .finally(() => { pending = undefined; });
56
+ return pending;
57
+ }
58
+
59
+ export { handleHookTrust } from "./relay";
@@ -0,0 +1,44 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { ProviderHook, type ProviderHook as Hook } from "../../../../packages/protocol/messages/provider-hooks";
4
+
5
+ export function ownedHookCommands() {
6
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
7
+ const base = `node "${join(root, "bin", "granttap-mcp.mjs")}" internal hook`;
8
+ return { policy: `${base} codex-policy`, permission: `${base} codex` };
9
+ }
10
+
11
+ /** Only the two exact user-config hooks installed by this runtime are reviewable. */
12
+ export function projectCodexHooks(value: unknown): Hook[] {
13
+ const commands = ownedHookCommands();
14
+ const report = value as { data?: Array<{ hooks?: any[]; errors?: unknown[] }> } | null;
15
+ const data = Array.isArray(report?.data) ? report.data : [];
16
+ return (["PreToolUse", "PermissionRequest"] as const).map((event) => {
17
+ const missing: Hook = { event, trustStatus: data.length ? "missing" : "unknown", enabled: false };
18
+ if (data.length !== 1 || (data[0]?.errors?.length ?? 0) > 0) {
19
+ return { ...missing, trustStatus: "unknown" };
20
+ }
21
+ const rows = Array.isArray(data[0]?.hooks) ? data[0]!.hooks! : [];
22
+ const candidates = rows.filter((row) => row?.eventName === (event === "PreToolUse" ? "preToolUse" : "permissionRequest")
23
+ && row.handlerType === "command" && row.source === "user" && row.isManaged === false
24
+ && row.command === commands[event === "PreToolUse" ? "policy" : "permission"]
25
+ && row.matcher === ".*" && row.timeoutSec === (event === "PreToolUse" ? 30 : 120));
26
+ if (candidates.length !== 1) return missing;
27
+ const row = candidates[0];
28
+ const parsed = ProviderHook.safeParse({ event, enabled: row.enabled, trustStatus: row.trustStatus,
29
+ key: row.key, currentHash: row.currentHash, command: row.command });
30
+ if (!parsed.success || !parsed.data.key || !parsed.data.currentHash) {
31
+ return { ...missing, trustStatus: "unknown" };
32
+ }
33
+ return parsed.data;
34
+ });
35
+ }
36
+
37
+ export function codexPolicyReady(hooks?: Hook[]): boolean {
38
+ return hooks?.some(row => row.event === "PreToolUse" && row.enabled && row.trustStatus === "trusted") ?? false;
39
+ }
40
+
41
+ export function codexHooksReady(hooks?: Hook[]): boolean {
42
+ return hooks?.length === 2 && new Set(hooks.map(row => row.event)).size === 2
43
+ && hooks.every(row => row.enabled && row.trustStatus === "trusted");
44
+ }
@@ -0,0 +1,24 @@
1
+ import type { RelayClient } from "../../../../packages/core/relay-client";
2
+ import type { ProviderHookTrust } from "../../../../packages/protocol/messages/provider-hooks";
3
+ import { loadConfig, machineConfigPath } from "../config";
4
+ import { observedComputerId } from "../mesh/identity/computer";
5
+ import { trustCodexHook } from "./trust";
6
+ import { refreshCodexHooks } from "./index";
7
+
8
+ export async function handleHookTrust(
9
+ client: RelayClient, request: ProviderHookTrust, peerPublicKey: string | undefined,
10
+ publish: () => Promise<void>,
11
+ ): Promise<boolean> {
12
+ if (!peerPublicKey) return false;
13
+ try {
14
+ const machine = loadConfig(machineConfigPath());
15
+ if (machine.role !== "machine" || machine.room !== client.room
16
+ || (machine.peerPublicKey !== peerPublicKey && !machine.extraPeerPublicKeys?.includes(peerPublicKey))) return false;
17
+ const result = await trustCodexHook(request, observedComputerId());
18
+ if (!result) return false;
19
+ await client.sendToPeer(result, peerPublicKey);
20
+ if (result.ok) await refreshCodexHooks(true);
21
+ void publish().catch(() => {});
22
+ return true;
23
+ } catch { return false; }
24
+ }
@@ -0,0 +1,55 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import { homedir } from "node:os";
4
+ import { resolveCodexBinary } from "../providers/codex-bin";
5
+
6
+ export type HookRpcOptions = { binary?: string; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number };
7
+ export type HookRpc = (method: string, params: unknown) => Promise<unknown>;
8
+
9
+ /** No threads or turns are created, and no hook commands are executed. */
10
+ export function codexHookRpc(options: HookRpcOptions = {}): HookRpc {
11
+ return (method, params) => new Promise((resolve, reject) => {
12
+ const env = options.env ?? process.env;
13
+ const codexHome = env.GRANTTAP_CODEX_DIR ?? env.NODVOX_CODEX_DIR ?? env.CODEX_HOME;
14
+ const child = spawn(options.binary ?? resolveCodexBinary(homedir(), env), ["app-server", "--listen", "stdio://"], {
15
+ cwd: options.cwd ?? homedir(), env: { ...env, ...(codexHome ? { CODEX_HOME: codexHome } : {}) },
16
+ stdio: ["pipe", "pipe", "ignore"],
17
+ });
18
+ const lines = createInterface({ input: child.stdout });
19
+ let bytes = 0;
20
+ let finished = false;
21
+ const finish = (value?: unknown, failed = false) => {
22
+ if (finished) return;
23
+ finished = true;
24
+ clearTimeout(timer);
25
+ lines.close();
26
+ child.stdin.destroy();
27
+ child.kill("SIGTERM");
28
+ if (failed) reject(new Error("Codex hook status is unavailable."));
29
+ else resolve(value);
30
+ };
31
+ const timer = setTimeout(() => finish(undefined, true), options.timeoutMs ?? 5_000);
32
+ const send = (message: unknown) => child.stdin.write(JSON.stringify(message) + "\n");
33
+ child.stdin.on("error", () => finish(undefined, true));
34
+ child.on("error", () => finish(undefined, true));
35
+ child.on("close", () => finish(undefined, true));
36
+ lines.on("line", (line) => {
37
+ bytes += Buffer.byteLength(line);
38
+ if (bytes > 2_000_000) return finish(undefined, true);
39
+ let message: any;
40
+ try { message = JSON.parse(line); } catch { return; }
41
+ if (!message || typeof message !== "object" || Array.isArray(message)) return;
42
+ if (message.id === 1) {
43
+ if (message.error) return finish(undefined, true);
44
+ send({ method: "initialized", params: {} });
45
+ send({ id: 2, method, params });
46
+ } else if (message.id === 2) {
47
+ finish(message.result, Boolean(message.error));
48
+ }
49
+ });
50
+ send({ id: 1, method: "initialize", params: {
51
+ clientInfo: { name: "granttap_hook_review", title: "GrantTap", version: "0.8.28" },
52
+ capabilities: { experimentalApi: true },
53
+ } });
54
+ });
55
+ }
@@ -0,0 +1,42 @@
1
+ import { homedir } from "node:os";
2
+ import { ProviderHookTrust, type ProviderHookTrustResult } from "../../../../packages/protocol/messages/provider-hooks";
3
+ import { codexHookRpc, type HookRpc } from "./rpc";
4
+ import { projectCodexHooks } from "./projection";
5
+
6
+ /** A person approves one displayed definition; no bulk trust or sandbox bypass. */
7
+ export async function trustCodexHook(
8
+ input: unknown, endpointId: string, rpc: HookRpc = codexHookRpc(), now = Date.now(),
9
+ ): Promise<ProviderHookTrustResult | undefined> {
10
+ const parsed = ProviderHookTrust.safeParse(input);
11
+ if (!parsed.success) return undefined;
12
+ const request = parsed.data;
13
+ let hooks = projectCodexHooks(null);
14
+ const result = (ok: boolean, message: string): ProviderHookTrustResult => ({
15
+ type: "provider.hook.trust.result", agent: "codex", endpointId, requestId: request.requestId,
16
+ ok, message, hooks, checkedAt: Date.now(),
17
+ });
18
+ if (request.endpointId !== endpointId || Math.abs(now - request.createdAt) > 5 * 60_000) {
19
+ return result(false, "Hook approval expired or targets another computer. Refresh and review again.");
20
+ }
21
+ try {
22
+ hooks = projectCodexHooks(await rpc("hooks/list", { cwds: [homedir()] }));
23
+ const current = hooks.find(row => row.event === request.event);
24
+ if (!current?.command || current.key !== request.key || current.currentHash !== request.currentHash) {
25
+ return result(false, "The hook changed or is unavailable. Refresh and review the current definition.");
26
+ }
27
+ const keyPath = `hooks.state.${JSON.stringify(current.key)}`;
28
+ const write = await rpc("config/batchWrite", { edits: [
29
+ { keyPath: `${keyPath}.trusted_hash`, value: current.currentHash, mergeStrategy: "upsert" },
30
+ { keyPath: `${keyPath}.enabled`, value: true, mergeStrategy: "upsert" },
31
+ ], filePath: null, expectedVersion: null, reloadUserConfig: true }) as { status?: string };
32
+ if (write?.status !== "ok") return result(false, "Codex could not apply hook trust. Check its configuration layers.");
33
+ hooks = projectCodexHooks(await rpc("hooks/list", { cwds: [homedir()] }));
34
+ const applied = hooks.find(row => row.event === request.event);
35
+ return applied?.enabled && applied.trustStatus === "trusted" && applied.key === current.key
36
+ && applied.currentHash === current.currentHash
37
+ ? result(true, "Codex confirmed this GrantTap hook is trusted and enabled.")
38
+ : result(false, "Codex has not confirmed this hook is active. Refresh and review again.");
39
+ } catch {
40
+ return result(false, "Codex hook approval is unavailable. Retry or review the hook in Codex Settings → Hooks.");
41
+ }
42
+ }