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.
- package/README.md +62 -11
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/apps/bridge/src/approvals/index.ts +2 -1
- package/apps/bridge/src/bin/cli/setup.ts +5 -2
- package/apps/bridge/src/bin/hooks/cursor-after-shell.ts +2 -1
- package/apps/bridge/src/bin/runtime/monitor.ts +6 -1
- package/apps/bridge/src/capabilities/skills.ts +17 -1
- package/apps/bridge/src/codex-hook-trust/README.md +34 -0
- package/apps/bridge/src/codex-hook-trust/config.ts +79 -0
- package/apps/bridge/src/codex-hook-trust/index.ts +59 -0
- package/apps/bridge/src/codex-hook-trust/projection.ts +44 -0
- package/apps/bridge/src/codex-hook-trust/relay.ts +24 -0
- package/apps/bridge/src/codex-hook-trust/rpc.ts +55 -0
- package/apps/bridge/src/codex-hook-trust/trust.ts +42 -0
- package/apps/bridge/src/cortex/integration.ts +6 -4
- package/apps/bridge/src/device-network/README.md +18 -0
- package/apps/bridge/src/device-network/directory.ts +39 -0
- package/apps/bridge/src/device-network/own-relay.ts +121 -0
- package/apps/bridge/src/device-network/settings.ts +52 -0
- package/apps/bridge/src/engine/README.md +5 -2
- package/apps/bridge/src/engine/protocol/impact/engine-impact-protocol.ts +74 -0
- package/apps/bridge/src/engine/protocol/transport/identity-result-parser.ts +26 -0
- package/apps/bridge/src/engine/protocol/transport/project-catalog-parser.ts +24 -0
- package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +7 -0
- package/apps/bridge/src/engine/protocol/transport/result-parser.ts +7 -17
- package/apps/bridge/src/engine/runtime/engine-client.ts +3 -0
- package/apps/bridge/src/engine/runtime/engine-projects.ts +4 -1
- package/apps/bridge/src/engine/runtime/repository-graph-jobs.ts +16 -0
- package/apps/bridge/src/host/self-protection.ts +3 -25
- package/apps/bridge/src/install/hooks.ts +5 -34
- package/apps/bridge/src/install/inspect.ts +12 -78
- package/apps/bridge/src/machine-load/README.md +17 -3
- package/apps/bridge/src/machine-load/command-process/README.md +13 -0
- package/apps/bridge/src/machine-load/command-process/history.ts +205 -0
- package/apps/bridge/src/machine-load/host/agent-load-history.ts +14 -11
- package/apps/bridge/src/machine-load/host/loop.ts +1 -1
- package/apps/bridge/src/machine-load/host/process-ownership.ts +115 -0
- package/apps/bridge/src/machine-load/host/process-sampler.ts +23 -177
- package/apps/bridge/src/machine-load/host/ps-reader.ts +93 -0
- package/apps/bridge/src/machine-load/index.ts +3 -0
- package/apps/bridge/src/machine-load/mcp/refresh.ts +1 -5
- package/apps/bridge/src/mesh/README.md +9 -0
- package/apps/bridge/src/mesh/catalog/README.md +22 -0
- package/apps/bridge/src/mesh/catalog/index.ts +15 -13
- package/apps/bridge/src/mesh/catalog/models.ts +17 -2
- package/apps/bridge/src/mesh/identity/computer.ts +5 -0
- package/apps/bridge/src/mesh/repository-details/README.md +25 -0
- package/apps/bridge/src/mesh/repository-details/index.ts +61 -0
- package/apps/bridge/src/mesh/restrictions/README.md +14 -0
- package/apps/bridge/src/mesh/restrictions/evaluate.ts +15 -0
- package/apps/bridge/src/mesh/runtime/execution-policy.ts +56 -9
- package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +13 -5
- package/apps/bridge/src/mesh/runtime/index.ts +54 -69
- package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +50 -0
- package/apps/bridge/src/mesh/tasks/create-task.ts +10 -4
- package/apps/bridge/src/monitor/handlers/catalog.ts +5 -1
- package/apps/bridge/src/monitor/handlers/inbound.ts +5 -1
- package/apps/bridge/src/monitor/handlers/user-message.ts +19 -10
- package/apps/bridge/src/monitor/support/runtime.ts +9 -63
- package/apps/bridge/src/monitor/support/session-activity.ts +8 -1
- package/apps/bridge/src/pairing/index.ts +7 -4
- package/apps/bridge/src/policy/effective-action.ts +33 -3
- package/apps/bridge/src/policy/impact/impact-evidence.ts +51 -0
- package/apps/bridge/src/policy/index.ts +2 -34
- package/apps/bridge/src/policy/shell/README.md +11 -0
- package/apps/bridge/src/policy/shell/safe-shell.ts +41 -0
- package/apps/bridge/src/project-policy/apply.ts +39 -12
- package/apps/bridge/src/project-policy/runtime.ts +16 -13
- package/apps/bridge/src/project-policy/types.ts +2 -2
- package/apps/bridge/src/provider-model-catalog/README.md +22 -0
- package/apps/bridge/src/provider-model-catalog/index.ts +63 -0
- package/apps/bridge/src/providers/codex-bin.ts +7 -2
- package/apps/bridge/src/sessions/cursor/scan.ts +3 -6
- package/apps/bridge/src/sessions/cursor/transcripts.ts +1 -3
- package/apps/bridge/src/sessions/index.ts +8 -9
- package/apps/bridge/src/sessions/scan/claude/activity.ts +16 -6
- package/apps/bridge/src/sessions/scan/claude/message-identity.ts +9 -0
- package/apps/bridge/src/sessions/scan/claude/parse.ts +11 -3
- package/apps/bridge/src/sessions/scan/claude/repository-observation/index.ts +16 -0
- package/apps/bridge/src/sessions/scan/claude/shared.ts +36 -16
- package/apps/bridge/src/sessions/scan/codex/activity/rows.ts +111 -0
- package/apps/bridge/src/sessions/scan/codex/activity.ts +15 -110
- package/apps/bridge/src/sessions/scan/codex/image/index.ts +44 -0
- package/apps/bridge/src/sessions/scan/codex/parse.ts +9 -4
- package/apps/bridge/src/sessions/scan/codex/repository-observation/index.ts +21 -0
- package/apps/bridge/src/sessions/scan/codex/shared.ts +36 -52
- package/apps/bridge/src/sessions/scan/codex/turn-state/index.ts +35 -0
- package/apps/bridge/src/sessions/support/activity-helpers.ts +8 -3
- package/apps/bridge/src/sessions/support/capability-totals.ts +0 -0
- package/apps/bridge/src/sessions/support/repository-checkout/index.ts +40 -0
- package/apps/bridge/src/sessions/telemetry/README.md +7 -3
- package/apps/bridge/src/sessions/telemetry/command-preview.ts +11 -5
- package/apps/bridge/src/sessions/telemetry/index.ts +7 -50
- package/apps/bridge/src/sessions/telemetry/wire-budget.ts +53 -0
- package/apps/bridge/src/transcript-history/README.md +28 -0
- package/apps/bridge/src/transcript-history/changes.ts +93 -0
- package/apps/bridge/src/transcript-history/images.ts +29 -0
- package/apps/bridge/src/transcript-history/index.ts +82 -0
- package/apps/bridge/src/transcript-history/native-lines.ts +37 -0
- package/apps/bridge/src/transcript-history/tool-details.ts +34 -0
- package/apps/bridge/src/transcript-history/turn-changes.ts +71 -0
- package/apps/mcp/src/bin/serve.ts +8 -3
- package/apps/mcp/src/bin/status.ts +2 -0
- package/apps/mcp/src/connection-center/README.md +5 -4
- package/apps/mcp/src/connection-center/state.ts +1 -1
- package/apps/mcp/src/desktop/README.md +48 -0
- package/apps/mcp/src/desktop/delivery/README.md +16 -0
- package/apps/mcp/src/desktop/delivery/attachments.ts +37 -0
- package/apps/mcp/src/desktop/delivery/mesh-create.ts +45 -0
- package/apps/mcp/src/desktop/delivery/task-create.ts +98 -0
- package/apps/mcp/src/desktop/delivery/task-send.ts +63 -0
- package/apps/mcp/src/desktop/engine-bridge.ts +146 -0
- package/apps/mcp/src/desktop/image/artifacts.ts +97 -0
- package/apps/mcp/src/desktop/image/index.ts +29 -0
- package/apps/mcp/src/desktop/invocation/index.ts +39 -0
- package/apps/mcp/src/desktop/live-catalog/README.md +10 -0
- package/apps/mcp/src/desktop/live-catalog/index.ts +40 -0
- package/apps/mcp/src/desktop/load/index.ts +35 -0
- package/apps/mcp/src/desktop/mesh-project.ts +123 -0
- package/apps/mcp/src/desktop/models/README.md +12 -0
- package/apps/mcp/src/desktop/models/index.ts +13 -0
- package/apps/mcp/src/desktop/native-access/README.md +19 -0
- package/apps/mcp/src/desktop/native-access/access.ts +67 -0
- package/apps/mcp/src/desktop/native-access/invoke.ts +34 -0
- package/apps/mcp/src/desktop/native-access/routes.ts +57 -0
- package/apps/mcp/src/desktop/network/README.md +17 -0
- package/apps/mcp/src/desktop/network/index.ts +28 -0
- package/apps/mcp/src/desktop/operations/index.ts +86 -0
- package/apps/mcp/src/desktop/pairing/README.md +22 -0
- package/apps/mcp/src/desktop/pairing/index.ts +83 -0
- package/apps/mcp/src/desktop/policy/README.md +17 -0
- package/apps/mcp/src/desktop/policy/auto-accept.ts +25 -0
- package/apps/mcp/src/desktop/policy/index.ts +42 -0
- package/apps/mcp/src/desktop/policy/scope.ts +20 -0
- package/apps/mcp/src/desktop/process/bridge-child.ts +20 -0
- package/apps/mcp/src/desktop/process/bridge-process.ts +61 -0
- package/apps/mcp/src/desktop/project-catalog.ts +30 -0
- package/apps/mcp/src/desktop/projection/installed-skills.ts +38 -0
- package/apps/mcp/src/desktop/projection/selected-mesh.ts +33 -0
- package/apps/mcp/src/desktop/projection/session-usage.ts +18 -0
- package/apps/mcp/src/desktop/projection/usage.ts +20 -0
- package/apps/mcp/src/desktop/provider-hooks/README.md +15 -0
- package/apps/mcp/src/desktop/provider-hooks/index.ts +12 -0
- package/apps/mcp/src/desktop/provider-storage/README.md +29 -0
- package/apps/mcp/src/desktop/provider-storage/fingerprint.ts +19 -0
- package/apps/mcp/src/desktop/provider-storage/index.ts +117 -0
- package/apps/mcp/src/desktop/provider-storage/scanner.ts +31 -0
- package/apps/mcp/src/desktop/task-activity-runner.ts +178 -0
- package/apps/mcp/src/desktop/task-activity-worker.ts +60 -0
- package/apps/mcp/src/desktop/task-activity.ts +84 -0
- package/apps/mcp/src/http/http-server.ts +27 -4
- package/apps/mcp/src/http-service/README.md +10 -0
- package/apps/mcp/src/http-service/installer.ts +10 -2
- package/apps/mcp/src/mcp-tools/connect/relay.ts +6 -1
- package/apps/mcp/src/oauth/README.md +1 -1
- package/apps/mcp/src/oauth/consent/website-session.ts +6 -4
- package/apps/mcp/src/oauth/session/connect-snapshot.ts +4 -1
- package/apps/mcp/src/oauth-provider.ts +14 -8
- package/apps/mcp/src/status/desktop-status.ts +32 -0
- package/apps/mcp/src/status/provider-status.ts +18 -4
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/docs/images/iphone-company-accounts.png +0 -0
- package/docs/images/iphone-company-device-invite.png +0 -0
- package/docs/images/iphone-company-repositories.png +0 -0
- package/package.json +1 -1
- package/packages/core/relay-client-types.ts +2 -0
- package/packages/protocol/messages/capabilities.ts +12 -0
- package/packages/protocol/messages/interaction/index.ts +2 -0
- package/packages/protocol/messages/mesh/index.ts +1 -0
- package/packages/protocol/messages/mesh/repository-details/index.ts +24 -0
- package/packages/protocol/messages/mesh/snapshot.ts +10 -0
- package/packages/protocol/messages/provider-hooks/README.md +20 -0
- package/packages/protocol/messages/provider-hooks/index.ts +53 -0
- package/packages/protocol/messages/sessions/index.ts +6 -12
- package/packages/protocol/schema.ts +4 -0
package/README.md
CHANGED
|
@@ -4,24 +4,26 @@
|
|
|
4
4
|
[](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
GrantTap is a
|
|
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.
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
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>
|
|
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.
|
|
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
|
|
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).
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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
|
-
?
|
|
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
|
|
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
|
+
}
|