granttap-mcp 0.2.0 → 0.6.4
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 +165 -68
- package/SECURITY.md +57 -1
- package/apps/bridge/src/approval.ts +12 -1
- package/apps/bridge/src/bin/connect.ts +27 -56
- package/apps/bridge/src/bin/monitor.ts +37 -0
- package/apps/bridge/src/bin/setup.ts +3 -1
- package/apps/bridge/src/capabilities.ts +424 -0
- package/apps/bridge/src/codex-control.ts +104 -0
- package/apps/bridge/src/config.ts +27 -1
- package/apps/bridge/src/delivery.ts +39 -0
- package/apps/bridge/src/install.ts +178 -5
- package/apps/bridge/src/monitor.ts +429 -0
- package/apps/bridge/src/pairing.ts +101 -0
- package/apps/bridge/src/reply.ts +417 -0
- package/apps/bridge/src/scheduler.ts +296 -0
- package/apps/bridge/src/session-keys.ts +64 -0
- package/apps/bridge/src/sessions.ts +669 -0
- package/apps/mcp/src/server.ts +101 -7
- package/bin/granttap-mcp.mjs +3 -0
- package/package.json +3 -3
- package/packages/core/crypto.ts +24 -27
- package/packages/core/relay-client.ts +61 -5
- package/packages/protocol/schema.ts +301 -1
- package/docs/images/phone-activity.png +0 -0
- package/docs/images/watch-approval.png +0 -0
package/README.md
CHANGED
|
@@ -1,38 +1,95 @@
|
|
|
1
1
|
# GrantTap MCP
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/granttap-mcp)
|
|
4
|
-
[](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**Keep Codex and Claude Code moving from your iPhone or Apple Watch.**
|
|
9
|
+
|
|
10
|
+
Approve commands, follow active tasks, reply, attach files, start new work, and
|
|
11
|
+
run local schedules without exposing agent traffic to the relay. GrantTap MCP is
|
|
12
|
+
the open-source machine-side bridge: the agents stay on your Mac, and Cloudflare
|
|
13
|
+
only routes authenticated ciphertext it cannot decrypt.
|
|
14
|
+
|
|
15
|
+
[Website](https://granttap.com) ·
|
|
16
|
+
[npm](https://www.npmjs.com/package/granttap-mcp) ·
|
|
17
|
+
[Security model](SECURITY.md) ·
|
|
18
|
+
[Self-hostable relay](https://github.com/sergii-ziborov/granttap-relay)
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-command-center.png" alt="GrantTap command center on iPhone showing a Codex approval, an agent question, task search, agent switcher, chat history, and MCP usage" width="330">
|
|
22
|
+
|
|
23
|
+
<img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-approval.png" alt="GrantTap Codex approval on Apple Watch" width="230">
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center"><sub>Current iPhone and Apple Watch UI captured from the app's clearly labelled demo mode — no concept renders.</sub></p>
|
|
27
|
+
|
|
28
|
+
The companion iPhone and Apple Watch app is currently in testing. Release
|
|
29
|
+
status is published at [granttap.com](https://granttap.com).
|
|
30
|
+
|
|
31
|
+
## What you get
|
|
32
|
+
|
|
33
|
+
- **Real approvals away from the Mac.** Claude Code or Codex pauses at its
|
|
34
|
+
permission hook; Allow or Deny returns to that same agent flow.
|
|
35
|
+
- **One task view for both agents.** See recent Codex and Claude Code tasks,
|
|
36
|
+
human-readable activity, delivery state, usage, and context data the agent
|
|
37
|
+
actually exposes.
|
|
38
|
+
- **Continue work from the phone.** Reply to an existing task or create a new
|
|
39
|
+
one for the selected agent and advertised workspace. Send up to five photos,
|
|
40
|
+
camera images, or documents in one message.
|
|
41
|
+
- **A useful Watch app, not an approval-only notification.** Browse tasks, open
|
|
42
|
+
recent activity, approve a command, or reply by voice or text.
|
|
43
|
+
- **Per-task controls that are enforced locally.** Inspect MCP servers and
|
|
44
|
+
repository skills, disable MCP servers for later GrantTap-delivered turns,
|
|
45
|
+
choose a relevant MCP or skill, and select Codex sandbox access.
|
|
46
|
+
- **A local scheduler, not a pretend provider API.** Create recurring Codex or
|
|
47
|
+
Claude Code runs manually or through a conversational planner, then inspect
|
|
48
|
+
the run history from the phone.
|
|
49
|
+
- **Receipts instead of optimistic UI.** Messages have stable IDs, encrypted
|
|
50
|
+
accepted/rejected receipts, bounded retries, and queued/sending/delivered/
|
|
51
|
+
failed state.
|
|
52
|
+
|
|
53
|
+
## See it in action
|
|
54
|
+
|
|
55
|
+
### iPhone
|
|
56
|
+
|
|
57
|
+
| Command center | Codex task controls | Observed MCP usage | Claude Code and scheduler |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-command-center.png" alt="GrantTap iPhone command center" width="210"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-task-detail.png" alt="Codex task context, MCP permissions, and sandbox access in GrantTap" width="210"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-mcp-usage.png" alt="Observed MCP and skill usage on iPhone" width="210"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-claude-tasks.png" alt="Claude Code task list and conversational scheduler on iPhone" width="210"> |
|
|
60
|
+
|
|
61
|
+
The Codex task screen exposes the context window, supported compaction, MCP
|
|
62
|
+
allow/deny state, and sandbox access. Claude Code gets its own visual treatment
|
|
63
|
+
and planner while keeping its different permission and compaction limits clear.
|
|
64
|
+
MCP usage counts only observed calls; context figures are labelled estimates,
|
|
65
|
+
not separate MCP billing.
|
|
66
|
+
|
|
67
|
+
### Apple Watch
|
|
68
|
+
|
|
69
|
+
| Task inbox | Recent activity and reply | Command approval |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-inbox.png" alt="GrantTap task and approval inbox on Apple Watch" width="230"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-task.png" alt="GrantTap task activity with voice and text reply on Apple Watch" width="230"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-approval.png" alt="Codex command approval on Apple Watch" width="230"> |
|
|
72
|
+
|
|
73
|
+
## Connect in under a minute
|
|
74
|
+
|
|
75
|
+
Add GrantTap to each agent you use:
|
|
5
76
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
GrantTap MCP complements the agents' native permission hooks:
|
|
11
|
-
|
|
12
|
-
- MCP is the voluntary channel: the agent calls `ask`, `ask_yes_no`, or `notify`.
|
|
13
|
-
- Hooks are the mandatory approval channel: Claude Code or Codex pauses before
|
|
14
|
-
a tool call and waits for Allow or Deny.
|
|
15
|
-
- Both channels use the same end-to-end encrypted GrantTap pairing.
|
|
16
|
-
|
|
17
|
-
| Visible iPhone activity | Approval on Apple Watch |
|
|
18
|
-
| --- | --- |
|
|
19
|
-
|  |  |
|
|
20
|
-
|
|
21
|
-
The iPhone and Apple Watch app is preparing for App Store release. Current
|
|
22
|
-
status and target date are published at [granttap.com](https://granttap.com).
|
|
23
|
-
Support, privacy, terms, and license information are linked below and from the
|
|
24
|
-
app's About screen.
|
|
77
|
+
```bash
|
|
78
|
+
codex mcp add granttap -- npx -y granttap-mcp@latest
|
|
79
|
+
claude mcp add granttap -- npx -y granttap-mcp@latest
|
|
80
|
+
```
|
|
25
81
|
|
|
26
|
-
|
|
82
|
+
Start a fresh agent task and say **“Connect GrantTap.”** The `connect` tool:
|
|
27
83
|
|
|
28
|
-
|
|
84
|
+
1. creates a new end-to-end encrypted pairing;
|
|
85
|
+
2. returns a scannable one-time QR directly in the agent chat;
|
|
86
|
+
3. installs the Codex and Claude Code approval hooks; and
|
|
87
|
+
4. installs the per-user background helper for task sync and schedules.
|
|
29
88
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
claude mcp add granttap -- npx -y granttap-mcp
|
|
33
|
-
```
|
|
89
|
+
Scan the QR with GrantTap on iPhone. No terminal QR, copied pairing JSON, or
|
|
90
|
+
open background terminal is required.
|
|
34
91
|
|
|
35
|
-
|
|
92
|
+
If an MCP client cannot render image content, use the CLI fallback:
|
|
36
93
|
|
|
37
94
|
```bash
|
|
38
95
|
npm install -g granttap-mcp
|
|
@@ -40,51 +97,92 @@ granttap-mcp connect
|
|
|
40
97
|
granttap-mcp setup
|
|
41
98
|
```
|
|
42
99
|
|
|
43
|
-
`connect` prints a QR and
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
100
|
+
CLI `connect` prints a one-time QR and short manual code. `setup` is idempotent:
|
|
101
|
+
it preserves unrelated agent settings and backs up a configuration file before
|
|
102
|
+
changing it. The pairing is stored locally in `~/.granttap/machine.json`.
|
|
103
|
+
Existing beta state under `~/.nodvox/` is migrated automatically.
|
|
104
|
+
|
|
105
|
+
## Codex and Claude Code: honest capability matrix
|
|
106
|
+
|
|
107
|
+
| Capability | Codex | Claude Code |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| Approval hook | `PermissionRequest` when Codex hooks are enabled | `PreToolUse` |
|
|
110
|
+
| Resume an existing task | Yes, through the local Codex CLI | Yes, through the local Claude CLI |
|
|
111
|
+
| Start a new persistent task | Yes | Yes |
|
|
112
|
+
| Up to five attachments | Images through Codex image inputs; documents as local paths | Local image/document paths in the turn |
|
|
113
|
+
| Change filesystem access from iPhone | Read-only, workspace, or full for the next GrantTap turn | Not exposed; the existing Claude policy remains authoritative |
|
|
114
|
+
| Disable MCP per task | Enforced for later GrantTap-delivered turns | Enforced for later GrantTap-delivered turns |
|
|
115
|
+
| Usage and context | Reported when present in local task logs | Reported when present in local task logs |
|
|
116
|
+
| Trigger real context compaction | Yes, for an idle task through Codex app-server | No supported remote API; GrantTap reports this honestly |
|
|
117
|
+
| Conversational schedule planner | Ephemeral read-only run | Ephemeral plan-mode run |
|
|
118
|
+
|
|
119
|
+
GrantTap controls local Codex and Claude Code tasks. It does **not** claim to
|
|
120
|
+
create ordinary ChatGPT chats, private ChatGPT Scheduled Tasks, Codex
|
|
121
|
+
Automations, or Claude Routines through unpublished provider APIs.
|
|
122
|
+
|
|
123
|
+
Globally disabled MCP servers stay disabled. A phone choice only narrows later
|
|
124
|
+
turns sent through GrantTap; it cannot broaden the agent's global configuration.
|
|
125
|
+
Repository skills are discovered only in the selected task workspace under
|
|
126
|
+
`.agents/skills` or `.claude/skills`.
|
|
52
127
|
|
|
53
128
|
## MCP tools
|
|
54
129
|
|
|
55
130
|
| Tool | Result |
|
|
56
131
|
| --- | --- |
|
|
132
|
+
| `connect` | Creates a pairing and returns a secure one-time QR in chat |
|
|
57
133
|
| `ask` | Sends an open question and waits for a spoken or typed reply |
|
|
58
134
|
| `ask_yes_no` | Sends a yes/no question and waits for a tap |
|
|
59
|
-
| `notify` | Sends a non-blocking status
|
|
60
|
-
| `setup` | Registers
|
|
61
|
-
|
|
62
|
-
## CLI commands
|
|
63
|
-
|
|
64
|
-
| Command | Purpose |
|
|
65
|
-
| --- | --- |
|
|
66
|
-
| *(no command)* | Starts the GrantTap MCP stdio server |
|
|
67
|
-
| `connect [relayUrl]` | Creates an E2EE pairing and prints a QR/short code |
|
|
68
|
-
| `setup` | Registers the Claude Code and Codex approval hooks |
|
|
135
|
+
| `notify` | Sends a non-blocking status update |
|
|
136
|
+
| `setup` | Registers both approval hooks and the terminal-free helper |
|
|
69
137
|
|
|
70
138
|
The default answer timeout is three minutes. Override it with
|
|
71
139
|
`GRANTTAP_ASK_TIMEOUT_MS`.
|
|
72
140
|
|
|
73
|
-
##
|
|
141
|
+
## Why the relay cannot read a session
|
|
142
|
+
|
|
143
|
+
- Endpoint keys are generated locally. The relay never receives a device's
|
|
144
|
+
secret encryption key.
|
|
145
|
+
- Pairing hand-off uses an opaque mailbox ID plus an independent 256-bit
|
|
146
|
+
transfer key. Only the mailbox ID reaches Cloudflare; the key stays in the QR
|
|
147
|
+
or manual token.
|
|
148
|
+
- Every attached Codex or Claude Code task receives its own random 256-bit task
|
|
149
|
+
key. Disclosure of one task key cannot decrypt another task.
|
|
150
|
+
- Questions, commands, replies, attachments, approvals, scheduler drafts, and
|
|
151
|
+
task activity remain authenticated ciphertext across the network, relay,
|
|
152
|
+
Durable Objects, and APNs path.
|
|
153
|
+
- APNs carries only a content-neutral wake. It contains no title, prompt,
|
|
154
|
+
command, path, task kind, or response.
|
|
155
|
+
|
|
156
|
+
The relay can still observe operational metadata: opaque room/mailbox IDs, IP
|
|
157
|
+
addresses, timing, ciphertext sizes, and APNs device token/environment. A
|
|
158
|
+
compromised authorized endpoint can read the tasks explicitly granted to that
|
|
159
|
+
endpoint; cryptography cannot hide plaintext from a device authorized to show
|
|
160
|
+
it. The exact boundary and threat-model limits are documented in
|
|
161
|
+
[SECURITY.md](SECURITY.md).
|
|
162
|
+
|
|
163
|
+
## Task sync and scheduling
|
|
164
|
+
|
|
165
|
+
The background helper publishes a bounded window of recent local Codex and
|
|
166
|
+
Claude Code task metadata. Older chat metadata is available separately for up
|
|
167
|
+
to 90 days and 160 chats; full activity for a task is sent only after the phone
|
|
168
|
+
subscribes to it. Hidden reasoning is never converted into visible activity.
|
|
169
|
+
|
|
170
|
+
New phone-created tasks default to an isolated per-agent GrantTap workspace.
|
|
171
|
+
The phone can instead select a same-agent folder already advertised by a recent
|
|
172
|
+
local task. The helper rejects arbitrary unadvertised paths.
|
|
173
|
+
|
|
174
|
+
Schedules use standard five-field cron in the Mac's timezone. The app supports
|
|
175
|
+
hourly, daily, weekday, selected-weekday, and monthly series, plus enable,
|
|
176
|
+
disable, edit, delete, and run-now actions. Every run records local start/end
|
|
177
|
+
time, agent, status, result, and created task ID.
|
|
74
178
|
|
|
75
|
-
|
|
76
|
-
public-key boxes. The relay receives only routing metadata and opaque
|
|
77
|
-
ciphertext. It has no device secret key and cannot decrypt questions,
|
|
78
|
-
commands, replies, or approvals.
|
|
79
|
-
|
|
80
|
-
This repository intentionally includes the protocol, crypto client, relay
|
|
81
|
-
client, MCP server, and agent hook adapters so that the complete public
|
|
82
|
-
machine-side trust boundary can be reviewed.
|
|
179
|
+
## CLI commands
|
|
83
180
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
181
|
+
| Command | Purpose |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| *(no command)* | Starts the GrantTap MCP stdio server |
|
|
184
|
+
| `connect [relayUrl]` | Creates an E2EE pairing; optionally targets a self-hosted `wss://` relay |
|
|
185
|
+
| `setup` | Registers the Codex/Claude hooks and background helper |
|
|
88
186
|
|
|
89
187
|
## Development
|
|
90
188
|
|
|
@@ -99,16 +197,15 @@ npm run typecheck
|
|
|
99
197
|
```
|
|
100
198
|
|
|
101
199
|
Start the stdio server with `npm start`. Run `npm run setup` only on a machine
|
|
102
|
-
where you want
|
|
103
|
-
as `*.bak-granttap`.
|
|
200
|
+
where you want GrantTap hooks installed.
|
|
104
201
|
|
|
105
|
-
##
|
|
202
|
+
## Links
|
|
106
203
|
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
- Privacy
|
|
111
|
-
- Support
|
|
112
|
-
- Security policy
|
|
204
|
+
- [GrantTap product site](https://granttap.com)
|
|
205
|
+
- [granttap-mcp on npm](https://www.npmjs.com/package/granttap-mcp)
|
|
206
|
+
- [Public relay source](https://github.com/sergii-ziborov/granttap-relay)
|
|
207
|
+
- [Privacy](https://granttap.com/privacy)
|
|
208
|
+
- [Support](https://granttap.com/support)
|
|
209
|
+
- [Security policy](SECURITY.md)
|
|
113
210
|
|
|
114
211
|
GrantTap is not affiliated with Anthropic or OpenAI.
|
package/SECURITY.md
CHANGED
|
@@ -6,7 +6,7 @@ Use GitHub's private
|
|
|
6
6
|
[security advisory form](https://github.com/sergii-ziborov/granttap-mcp/security/advisories/new).
|
|
7
7
|
For urgent coordination, contact `sergii.ziborov@gmail.com`.
|
|
8
8
|
|
|
9
|
-
Do not open a public issue containing pairing
|
|
9
|
+
Do not open a public issue containing pairing tokens, device keys, room
|
|
10
10
|
identifiers, real prompts, command payloads, local configuration, or a working
|
|
11
11
|
exploit. Include the affected package version or commit, expected impact, and
|
|
12
12
|
a minimal reproduction using synthetic data.
|
|
@@ -17,5 +17,61 @@ Pairing state belongs in `~/.granttap/` and must never be committed, attached
|
|
|
17
17
|
to an issue, or pasted into logs. The package avoids printing device secret
|
|
18
18
|
keys. Existing configuration files are backed up before hook installation.
|
|
19
19
|
|
|
20
|
+
Current pairings contain three independent secret classes: NaCl endpoint keys,
|
|
21
|
+
random per-task keys, and a random `pushAuth` room credential that only
|
|
22
|
+
authorizes APNs device-token registration at the relay. None may be logged.
|
|
23
|
+
The relay receives only a hash of `pushAuth` and never receives an endpoint's
|
|
24
|
+
secret encryption key.
|
|
25
|
+
|
|
26
|
+
## Cryptographic boundary
|
|
27
|
+
|
|
28
|
+
- Every device pairing is generated locally and has unique Curve25519 secret
|
|
29
|
+
keys. A device from another pairing cannot authenticate or decrypt it.
|
|
30
|
+
- Pairing hand-off v2 uses a relay-visible random 128-bit mailbox id and a
|
|
31
|
+
separate random 256-bit transfer key. Only the mailbox id is used in the HTTP
|
|
32
|
+
path. The key stays in the QR/manual token, so a relay operator or a Durable
|
|
33
|
+
Object database dump cannot open the parked pairing blob.
|
|
34
|
+
- Every attached Codex or Claude Code task receives a separate random 256-bit
|
|
35
|
+
task key. Task messages, attachments, visible activity, access/MCP changes,
|
|
36
|
+
compaction results, and task-bound approvals use this additional authenticated
|
|
37
|
+
encryption layer. A key copied from one task cannot decrypt another task.
|
|
38
|
+
- Task keys are delivered only inside the already authenticated device-to-device
|
|
39
|
+
NaCl channel and are stored in device-only Keychain on iOS and a mode `0600`
|
|
40
|
+
file on the agent Mac.
|
|
41
|
+
- Plaintext exists only at an authorized endpoint. Every route between the Mac
|
|
42
|
+
and iPhone remains authenticated ciphertext while it crosses the app
|
|
43
|
+
transport, network, Cloudflare, Durable Objects, and APNs.
|
|
44
|
+
- APNs is only a content-neutral wake. It contains no task kind, request id,
|
|
45
|
+
delivery id, title, prompt, command, path, or response.
|
|
46
|
+
- Scheduler-planner turns and their structured drafts cross Cloudflare only as
|
|
47
|
+
authenticated device-to-device ciphertext. The selected CLI runs locally in
|
|
48
|
+
ephemeral read-only/plan mode; it cannot silently turn planning into a
|
|
49
|
+
persistent task or workspace mutation.
|
|
50
|
+
|
|
51
|
+
Cloudflare can observe service metadata required to operate the relay: an
|
|
52
|
+
opaque room/mailbox id, routing role, timestamps/expiry, IP address, ciphertext
|
|
53
|
+
size, APNs device token/environment, and a content-neutral wake flag. It cannot
|
|
54
|
+
derive plaintext from those fields or from its stored database.
|
|
55
|
+
|
|
56
|
+
The honest limit: a device can decrypt every task key that was explicitly
|
|
57
|
+
granted to that device. Possession of a device authorized for task A alone
|
|
58
|
+
cannot decrypt another pairing or task B unless B's independent key was also
|
|
59
|
+
granted to it. Cryptography cannot both authorize a device for a task and
|
|
60
|
+
prevent that same device from reading it. Compromise of the agent Mac is
|
|
61
|
+
also outside the relay threat model because the Mac necessarily has the local
|
|
62
|
+
agent transcripts and keys. The isolation guarantee is against Cloudflare,
|
|
63
|
+
database/network compromise, other pairings/devices, and disclosure of a
|
|
64
|
+
different task's key—not against an already authorized endpoint.
|
|
65
|
+
|
|
66
|
+
The delivery ledger and scheduler history are stored only on the paired Mac.
|
|
67
|
+
They contain random message ids and local task metadata and are bounded and
|
|
68
|
+
expired; do not attach real copies to bug reports.
|
|
69
|
+
|
|
70
|
+
Chat history and capability metadata follow the same cryptographic boundary.
|
|
71
|
+
The bridge reads a bounded set of local Codex/Claude logs, emits only chat
|
|
72
|
+
metadata and visible activity, and places it inside the existing device or
|
|
73
|
+
independent per-task NaCl box. Structured MCP/skill fields describe observed
|
|
74
|
+
tool calls; hidden reasoning is never turned into activity or usage records.
|
|
75
|
+
|
|
20
76
|
Relay vulnerabilities should be reported through the
|
|
21
77
|
[GrantTap relay advisory form](https://github.com/sergii-ziborov/granttap-relay/security/advisories/new).
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { PeerConfig } from "../../../packages/core/relay-client";
|
|
9
9
|
import { RelayClient } from "../../../packages/core/relay-client";
|
|
10
10
|
import type { ApprovalDecision, ApprovalRequest, Payload } from "../../../packages/protocol/schema";
|
|
11
|
+
import { primeSessionKeys, sendSessionPayload } from "./session-keys";
|
|
11
12
|
|
|
12
13
|
export type RequestApprovalOpts = {
|
|
13
14
|
timeoutMs?: number;
|
|
@@ -23,6 +24,7 @@ export async function requestApproval(
|
|
|
23
24
|
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
24
25
|
const client = opts.client ?? new RelayClient(cfg);
|
|
25
26
|
const ownsClient = !opts.client;
|
|
27
|
+
primeSessionKeys(client);
|
|
26
28
|
|
|
27
29
|
try {
|
|
28
30
|
if (ownsClient) {
|
|
@@ -37,7 +39,16 @@ export async function requestApproval(
|
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
|
-
|
|
42
|
+
// The request id is random and carries no task content. Reusing it as the
|
|
43
|
+
// opaque delivery id lets a generic APNs action answer the request even if
|
|
44
|
+
// iOS has not yet pulled and decrypted the full card.
|
|
45
|
+
const sendOptions = {
|
|
46
|
+
ttlMs: timeoutMs,
|
|
47
|
+
wake: true,
|
|
48
|
+
deliveryId: req.requestId,
|
|
49
|
+
};
|
|
50
|
+
if (req.sessionId) await sendSessionPayload(client, req, req.sessionId, "phone", sendOptions);
|
|
51
|
+
else await client.send(req, "phone", sendOptions);
|
|
41
52
|
const decision = await client
|
|
42
53
|
.waitFor(
|
|
43
54
|
(p: Payload): p is ApprovalDecision =>
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import QRCode from "qrcode";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
createPairing,
|
|
6
|
-
machineConfigPath,
|
|
7
|
-
pairingUri,
|
|
8
|
-
phonePairingPath,
|
|
9
|
-
saveConfig,
|
|
10
|
-
} from "../config";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_RELAY = "wss://granttap-relay.sergii-ziborov.workers.dev";
|
|
2
|
+
import type { InstallResult } from "../install";
|
|
3
|
+
import { machineConfigPath, phonePairingPath } from "../config";
|
|
4
|
+
import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from "../pairing";
|
|
13
5
|
|
|
14
6
|
function hookLine(name: string, result: InstallResult): string {
|
|
15
7
|
switch (result.status) {
|
|
@@ -23,69 +15,48 @@ function hookLine(name: string, result: InstallResult): string {
|
|
|
23
15
|
}
|
|
24
16
|
|
|
25
17
|
async function connect(relayUrl: string): Promise<void> {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
saveConfig(phonePairingPath(), phoneCfg);
|
|
29
|
-
|
|
30
|
-
const uri = pairingUri(phoneCfg);
|
|
31
|
-
const qr = await QRCode.toString(uri, {
|
|
18
|
+
const pairing = await createOneTimePairing(relayUrl);
|
|
19
|
+
const qr = await QRCode.toString(pairing.qrPayload, {
|
|
32
20
|
type: "terminal",
|
|
33
21
|
small: true,
|
|
34
22
|
errorCorrectionLevel: "L",
|
|
35
23
|
});
|
|
36
|
-
process.stdout.write(["", " Scan this code in GrantTap on iPhone:", "", qr].join("\n"));
|
|
37
|
-
|
|
38
|
-
const code = generatePairingCode(8);
|
|
39
|
-
const sealed = sealWithCode(phoneCfg, code);
|
|
40
|
-
const httpBase = relayUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
|
|
41
|
-
let codeReady = false;
|
|
42
|
-
try {
|
|
43
|
-
const response = await fetch(`${httpBase}/pair/${code}`, {
|
|
44
|
-
method: "PUT",
|
|
45
|
-
headers: { "content-type": "application/json" },
|
|
46
|
-
body: JSON.stringify(sealed),
|
|
47
|
-
});
|
|
48
|
-
codeReady = response.ok;
|
|
49
|
-
} catch {
|
|
50
|
-
codeReady = false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
24
|
process.stdout.write(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"",
|
|
70
|
-
].join("\n"),
|
|
25
|
+
[
|
|
26
|
+
"",
|
|
27
|
+
" Scan this one-time code in GrantTap on iPhone:",
|
|
28
|
+
"",
|
|
29
|
+
qr,
|
|
30
|
+
"",
|
|
31
|
+
" Or paste this one-time secure token in the app:",
|
|
32
|
+
"",
|
|
33
|
+
` ${pairing.manualToken}`,
|
|
34
|
+
"",
|
|
35
|
+
` Relay: ${pairing.httpBase}`,
|
|
36
|
+
` The encrypted mailbox expires after ${PAIRING_CODE_TTL_MINUTES} minutes and can be used once.`,
|
|
37
|
+
" Its 256-bit key is never sent to the relay.",
|
|
38
|
+
"",
|
|
39
|
+
].join("\n"),
|
|
71
40
|
);
|
|
72
41
|
|
|
73
|
-
const skipHooks =
|
|
74
|
-
const claude = skipHooks ? null : installClaudeHook();
|
|
75
|
-
const codex = skipHooks ? null : installCodexHook();
|
|
42
|
+
const skipHooks = pairing.claude == null || pairing.codex == null;
|
|
76
43
|
process.stdout.write(
|
|
77
44
|
[
|
|
78
45
|
"",
|
|
79
46
|
" GrantTap is paired on this machine.",
|
|
80
47
|
"",
|
|
81
|
-
` room: ${machineCfg.room}`,
|
|
48
|
+
` room: ${pairing.machineCfg.room}`,
|
|
82
49
|
` relay: ${relayUrl}`,
|
|
83
50
|
` machine cfg: ${machineConfigPath()}`,
|
|
84
51
|
` phone cfg: ${phonePairingPath()} (manual fallback; keep private)`,
|
|
85
52
|
"",
|
|
86
53
|
...(skipHooks
|
|
87
54
|
? [" Hooks: skipped (GRANTTAP_SKIP_HOOKS=1)"]
|
|
88
|
-
: [
|
|
55
|
+
: [
|
|
56
|
+
hookLine("Claude Code", pairing.claude!),
|
|
57
|
+
hookLine("Codex", pairing.codex!),
|
|
58
|
+
hookLine("Background task sync", pairing.monitor!),
|
|
59
|
+
]),
|
|
89
60
|
"",
|
|
90
61
|
" Keep the MCP server configured in your agent. Approval requests now",
|
|
91
62
|
" use the paired phone and watch; if the relay is unavailable, hooks fall",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Persistent, terminal-free task sync for the GrantTap phone app on macOS. */
|
|
2
|
+
import { RelayClient } from "../../../../packages/core/relay-client";
|
|
3
|
+
import { loadConfig, machineConfigPath } from "../config";
|
|
4
|
+
import { startSessionMonitor } from "../monitor";
|
|
5
|
+
|
|
6
|
+
let client: RelayClient;
|
|
7
|
+
let monitor: ReturnType<typeof startSessionMonitor>;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
client = new RelayClient(loadConfig(machineConfigPath()), { autoReconnect: true });
|
|
11
|
+
monitor = startSessionMonitor(client);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
process.stderr.write(
|
|
14
|
+
`[granttap-mcp] monitor is not paired: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
15
|
+
);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stop = (): void => {
|
|
20
|
+
monitor.close();
|
|
21
|
+
client.close();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
process.on("SIGINT", stop);
|
|
26
|
+
process.on("SIGTERM", stop);
|
|
27
|
+
|
|
28
|
+
void client
|
|
29
|
+
.connect()
|
|
30
|
+
.then(() => monitor.publish())
|
|
31
|
+
.catch((error: unknown) => {
|
|
32
|
+
// RelayClient keeps reconnecting in the background. Log the initial error
|
|
33
|
+
// for diagnostics without terminating the launchd-managed helper.
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
`[granttap-mcp] initial relay connection failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { installClaudeHook, installCodexHook } from "../install";
|
|
1
|
+
import { installClaudeHook, installCodexHook, installMonitorHelper } from "../install";
|
|
2
2
|
|
|
3
3
|
const claude = installClaudeHook();
|
|
4
4
|
const codex = installCodexHook();
|
|
5
|
+
const monitor = installMonitorHelper();
|
|
5
6
|
|
|
6
7
|
process.stdout.write(
|
|
7
8
|
[
|
|
8
9
|
`Claude Code: ${claude.status} (${claude.detail})`,
|
|
9
10
|
`Codex: ${codex.status} (${codex.detail})`,
|
|
11
|
+
`Background task sync: ${monitor.status} (${monitor.detail})`,
|
|
10
12
|
].join("\n") + "\n",
|
|
11
13
|
);
|