granttap-mcp 0.7.6 → 0.8.1
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/LICENSE +86 -21
- package/README.md +33 -3
- package/THIRD_PARTY_NOTICES.md +23 -0
- package/apps/bridge/src/bin/claude-hook.ts +60 -55
- package/apps/bridge/src/bin/codex-hook.ts +17 -5
- package/apps/bridge/src/bin/codex-policy-hook.ts +30 -10
- package/apps/bridge/src/bin/cursor-hook.ts +53 -7
- package/apps/bridge/src/bin/cursor-mcp-hook.ts +97 -36
- package/apps/bridge/src/bin/monitor.ts +15 -1
- package/apps/bridge/src/bin/setup.ts +15 -1
- package/apps/bridge/src/capabilities/README.md +2 -0
- package/apps/bridge/src/config/README.md +3 -0
- package/apps/bridge/src/config/runtime.ts +26 -0
- package/apps/bridge/src/cursor-mcp-policy.ts +36 -1
- package/apps/bridge/src/engine/README.md +25 -0
- package/apps/bridge/src/engine/engine-client.ts +204 -0
- package/apps/bridge/src/engine/engine-declaration.ts +74 -0
- package/apps/bridge/src/engine/engine-health.ts +22 -0
- package/apps/bridge/src/engine/engine-policy-protocol.ts +262 -0
- package/apps/bridge/src/engine/engine-policy-types.ts +37 -0
- package/apps/bridge/src/engine/engine-projects.ts +68 -0
- package/apps/bridge/src/engine/engine-protocol.ts +265 -0
- package/apps/bridge/src/engine/engine-supervisor.ts +175 -0
- package/apps/bridge/src/install.ts +30 -4
- package/apps/bridge/src/machine-load/README.md +10 -0
- package/apps/bridge/src/machine-load/agent-load-history.ts +94 -0
- package/apps/bridge/src/machine-load/index.ts +130 -0
- package/apps/bridge/src/machine-load/loop.ts +149 -0
- package/apps/bridge/src/machine-load/mcp-load-cache.ts +46 -0
- package/apps/bridge/src/machine-load/mcp-load-refresh.ts +65 -0
- package/apps/bridge/src/machine-load/mcp-process-sampler.ts +69 -0
- package/apps/bridge/src/machine-load/process-sampler.ts +99 -0
- package/apps/bridge/src/machine-load/scan-cost.ts +25 -0
- package/apps/bridge/src/mesh/README.md +10 -0
- package/apps/bridge/src/mesh/binding-state.ts +87 -0
- package/apps/bridge/src/mesh/catalog.ts +80 -11
- package/apps/bridge/src/mesh/convergence.ts +7 -5
- package/apps/bridge/src/mesh/execution-sweep.ts +35 -0
- package/apps/bridge/src/mesh/identity.ts +11 -0
- package/apps/bridge/src/mesh/snapshot-merge.ts +29 -0
- package/apps/bridge/src/mesh/store-state.ts +33 -3
- package/apps/bridge/src/mesh/store.ts +58 -32
- package/apps/bridge/src/monitor-leadership.ts +139 -0
- package/apps/bridge/src/monitor.ts +36 -72
- package/apps/bridge/src/policy/README.md +23 -0
- package/apps/bridge/src/policy/capability-fingerprint.ts +211 -0
- package/apps/bridge/src/policy/codex-project-ask.ts +207 -0
- package/apps/bridge/src/policy/effective-action.ts +143 -0
- package/apps/bridge/src/project-policy/mapping.ts +156 -0
- package/apps/bridge/src/project-policy/runtime.ts +213 -0
- package/apps/bridge/src/reply/README.md +3 -0
- package/apps/bridge/src/reply/cursor-agent-bin.ts +46 -0
- package/apps/bridge/src/reply/provider-headless.ts +2 -1
- package/apps/bridge/src/reply/types.ts +3 -0
- package/apps/bridge/src/reply.ts +24 -5
- package/apps/bridge/src/session-keys.ts +16 -1
- package/apps/bridge/src/sessions/activity-helpers.ts +2 -1
- package/apps/bridge/src/sessions/capability-totals.ts +0 -0
- package/apps/bridge/src/sessions/claude.ts +1 -1
- package/apps/bridge/src/sessions/codex-head.ts +77 -0
- package/apps/bridge/src/sessions/codex.ts +22 -24
- package/apps/bridge/src/sessions/cursor/README.md +3 -0
- package/apps/bridge/src/sessions/cursor/transcripts.ts +1 -1
- package/apps/bridge/src/sessions/telemetry/README.md +3 -0
- package/apps/bridge/src/sessions/telemetry/identity.ts +1 -0
- package/apps/bridge/src/sessions/telemetry.ts +17 -0
- package/apps/bridge/src/sessions.ts +17 -5
- package/apps/mcp/src/http-server.ts +3 -2
- package/apps/mcp/src/http-service/README.md +2 -0
- package/apps/mcp/src/mcp-tools/README.md +3 -0
- package/apps/mcp/src/oauth/README.md +2 -0
- package/apps/mcp/src/oauth/loopback-origin.ts +16 -0
- package/bin/granttap-mcp.mjs +1 -1
- package/cursor-plugin/.cursor-plugin/plugin.json +1 -1
- package/cursor-plugin/LICENSE +86 -0
- package/cursor-plugin/README.md +5 -0
- package/docs/images/iphone-chat.png +0 -0
- package/docs/images/iphone-claude-tasks.png +0 -0
- package/docs/images/iphone-command-center.png +0 -0
- package/docs/images/iphone-mcp-usage.png +0 -0
- package/package.json +4 -3
- package/packages/protocol/messages/README.md +12 -0
- package/packages/protocol/messages/capabilities.ts +54 -0
- package/packages/protocol/messages/interaction.ts +9 -0
- package/packages/protocol/messages/machine.ts +29 -0
- package/packages/protocol/messages/mesh.ts +21 -1
- package/packages/protocol/messages/project-policy.ts +172 -0
- package/packages/protocol/schema.ts +12 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Serhii Ziborov
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
GrantTap Commercial Source License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serhii Ziborov. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This license governs the source code and software distributed from the
|
|
6
|
+
granttap-mcp repository and package (the "Software"), except for third-party
|
|
7
|
+
components that identify their own license terms.
|
|
8
|
+
|
|
9
|
+
1. Definitions
|
|
10
|
+
|
|
11
|
+
"GrantTap" means Serhii Ziborov doing business as GrantTap.
|
|
12
|
+
|
|
13
|
+
"Authorized Access" means an active paid GrantTap subscription, an official
|
|
14
|
+
GrantTap trial, or a separate written commercial agreement with GrantTap.
|
|
15
|
+
|
|
16
|
+
"Authorized User" means a person or legal entity covered by Authorized Access.
|
|
17
|
+
|
|
18
|
+
2. Source inspection and contribution
|
|
19
|
+
|
|
20
|
+
You may download, inspect, and make local modifications to the source solely to
|
|
21
|
+
evaluate the Software, perform security review, or prepare a contribution to
|
|
22
|
+
the official GrantTap repository. You may create a public fork solely for that
|
|
23
|
+
contribution workflow if the fork retains this license and does not publish a
|
|
24
|
+
release, binary, hosted service, or independently usable distribution.
|
|
25
|
+
|
|
26
|
+
These rights do not permit production use, commercial use, or use to provide a
|
|
27
|
+
service to another person.
|
|
28
|
+
|
|
29
|
+
3. Authorized product use
|
|
30
|
+
|
|
31
|
+
During Authorized Access, GrantTap grants the Authorized User a limited,
|
|
32
|
+
non-exclusive, non-transferable, non-sublicensable license to install, execute,
|
|
33
|
+
and internally modify the Software on devices the Authorized User owns or
|
|
34
|
+
controls, solely to use the official GrantTap product and services.
|
|
35
|
+
|
|
36
|
+
When Authorized Access ends, the Authorized User must stop using the Software
|
|
37
|
+
except where a separate written agreement says otherwise.
|
|
38
|
+
|
|
39
|
+
4. Restrictions
|
|
40
|
+
|
|
41
|
+
Unless GrantTap gives prior written permission, you may not:
|
|
42
|
+
|
|
43
|
+
- sell, sublicense, redistribute, or publish the Software or a derivative work;
|
|
44
|
+
- offer the Software or a derivative work as a hosted or managed service;
|
|
45
|
+
- use the Software to build or operate a competing product or service;
|
|
46
|
+
- use the Software as a standalone general-purpose coding-agent runtime outside
|
|
47
|
+
the official GrantTap product;
|
|
48
|
+
- bypass subscription, license, access-control, or entitlement checks;
|
|
49
|
+
- remove or alter copyright, license, attribution, or proprietary notices; or
|
|
50
|
+
- use GrantTap names or marks to imply endorsement of a modified distribution.
|
|
51
|
+
|
|
52
|
+
5. Ownership
|
|
53
|
+
|
|
54
|
+
The Software is licensed, not sold. GrantTap retains all right, title, and
|
|
55
|
+
interest in the Software and its derivative works. No trademark, patent, or
|
|
56
|
+
other rights are granted except the limited copyright rights stated here.
|
|
57
|
+
|
|
58
|
+
6. Third-party components
|
|
59
|
+
|
|
60
|
+
Third-party components remain governed by their respective licenses. Those
|
|
61
|
+
licenses and notices are not replaced by this license. See
|
|
62
|
+
THIRD_PARTY_NOTICES.md where distributed.
|
|
63
|
+
|
|
64
|
+
7. Termination
|
|
65
|
+
|
|
66
|
+
This license terminates automatically if you breach its terms. On termination,
|
|
67
|
+
you must stop using and distributing the Software and delete copies that you
|
|
68
|
+
are not legally required to retain. Sections 4 through 9 survive termination.
|
|
69
|
+
|
|
70
|
+
8. Disclaimer of warranty
|
|
71
|
+
|
|
72
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTIES OF
|
|
73
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
74
|
+
FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
|
|
75
|
+
|
|
76
|
+
9. Limitation of liability
|
|
77
|
+
|
|
78
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, GRANTTAP AND ITS CONTRIBUTORS WILL NOT
|
|
79
|
+
BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR
|
|
80
|
+
PUNITIVE DAMAGES, OR FOR LOSS OF DATA, PROFITS, REVENUE, OR BUSINESS, ARISING
|
|
81
|
+
FROM OR RELATED TO THE SOFTWARE OR THIS LICENSE.
|
|
82
|
+
|
|
83
|
+
10. Commercial terms
|
|
84
|
+
|
|
85
|
+
Authorized Access, pricing, and separate commercial licenses are available
|
|
86
|
+
through https://granttap.com.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/granttap-mcp)
|
|
4
4
|
[](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
|
|
5
|
-
[](LICENSE)
|
|
6
6
|
|
|
7
7
|
GrantTap is a Personal live control center for local coding agents.
|
|
8
8
|
|
|
@@ -13,6 +13,10 @@ hooks, local adapters, and TypeScript wire schemas. Agents and provider
|
|
|
13
13
|
credentials stay on your computer. Native iPhone and Apple Watch traffic is
|
|
14
14
|
end-to-end encrypted.
|
|
15
15
|
|
|
16
|
+
The source is public for inspection and contribution, but GrantTap MCP is
|
|
17
|
+
proprietary paid software rather than open source. Production use requires an
|
|
18
|
+
active GrantTap subscription or a separate commercial agreement.
|
|
19
|
+
|
|
16
20
|
[Website](https://granttap.com) · [npm](https://www.npmjs.com/package/granttap-mcp) ·
|
|
17
21
|
[Security model](SECURITY.md) ·
|
|
18
22
|
[Relay source](https://github.com/sergii-ziborov/granttap-relay)
|
|
@@ -97,6 +101,9 @@ history stays on the device.
|
|
|
97
101
|
|
|
98
102
|
## Install
|
|
99
103
|
|
|
104
|
+
Installation and production use require Authorized Access under the
|
|
105
|
+
[GrantTap Commercial Source License](LICENSE).
|
|
106
|
+
|
|
100
107
|
```bash
|
|
101
108
|
npm install -g granttap-mcp
|
|
102
109
|
granttap setup
|
|
@@ -107,10 +114,17 @@ hooks, installs the background helper, configures Cursor's persistent local
|
|
|
107
114
|
OAuth service when Cursor is present, and starts phone pairing when run in an
|
|
108
115
|
interactive terminal. It ends with one exact next action.
|
|
109
116
|
|
|
117
|
+
Setup also declares the separately distributed GrantTap Engine, which Project
|
|
118
|
+
Governance needs before it can report anything. The standard locations are
|
|
119
|
+
searched, and `--engine <path>` points at one that lives elsewhere; the binary
|
|
120
|
+
is checksummed here, because that checksum is the only thing verified before it
|
|
121
|
+
is launched. Without an engine the rollout stays off and setup says so, rather
|
|
122
|
+
than leaving "Governance not reported" on the phone as the only symptom.
|
|
123
|
+
|
|
110
124
|
The normal CLI surface is intentionally small:
|
|
111
125
|
|
|
112
126
|
```text
|
|
113
|
-
granttap setup
|
|
127
|
+
granttap setup [--engine <path>]
|
|
114
128
|
granttap status [--json]
|
|
115
129
|
granttap connect [--relay <wss-url>]
|
|
116
130
|
granttap reset [--yes]
|
|
@@ -162,11 +176,19 @@ The bounded encrypted protocol preserves:
|
|
|
162
176
|
- visible activity, delivery state, context and token counters;
|
|
163
177
|
- MCP, Skill, and CLI observations;
|
|
164
178
|
- child-agent relationships;
|
|
165
|
-
- per-capability outcome: `success`, `error`, `cancelled`, or `unknown
|
|
179
|
+
- per-capability outcome: `success`, `error`, `cancelled`, or `unknown`;
|
|
180
|
+
- what a call cost, where the machine can be observed while it ran.
|
|
166
181
|
|
|
167
182
|
An optional bounded `errorClass` may describe an error category. Full tool
|
|
168
183
|
error payloads are not copied into usage telemetry by default.
|
|
169
184
|
|
|
185
|
+
Cost is reported as attributed rather than measured, because that is what it
|
|
186
|
+
is. A call is read back from the transcript once it has finished, so it can
|
|
187
|
+
never be measured directly: an MCP server outlives its calls and is sampled
|
|
188
|
+
directly, while a built-in tool leaves nothing behind and is costed from the
|
|
189
|
+
samples that fall inside its own start and end. A call with no sample near it
|
|
190
|
+
reports nothing rather than a number borrowed from another moment.
|
|
191
|
+
|
|
170
192
|
## Local enforcement
|
|
171
193
|
|
|
172
194
|
GrantTap can narrow later actions for an exact task only where a provider
|
|
@@ -217,4 +239,12 @@ gate on macOS, and `npm publish` enforces the same gate through
|
|
|
217
239
|
Do not publish from a dirty checkout or before the package allowlist, tests,
|
|
218
240
|
typecheck, and release checks pass.
|
|
219
241
|
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
GrantTap MCP is distributed under the proprietary
|
|
245
|
+
[GrantTap Commercial Source License 1.0](LICENSE). Public source access does not
|
|
246
|
+
grant open-source, redistribution, hosted-service, or competing-product rights.
|
|
247
|
+
Third-party dependencies retain their own terms; see
|
|
248
|
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
249
|
+
|
|
220
250
|
GrantTap is not affiliated with Anthropic, OpenAI, Apple, Anysphere, or xAI.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
GrantTap MCP is proprietary software distributed under the GrantTap Commercial
|
|
4
|
+
Source License. Third-party components retain their own licenses; the GrantTap
|
|
5
|
+
license does not replace or restrict those terms.
|
|
6
|
+
|
|
7
|
+
The npm package declares, rather than vendors, these direct runtime
|
|
8
|
+
dependencies. Their packages carry the applicable license texts:
|
|
9
|
+
|
|
10
|
+
| Component | License |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `@modelcontextprotocol/sdk` | MIT |
|
|
13
|
+
| `express` | MIT |
|
|
14
|
+
| `qrcode` | MIT |
|
|
15
|
+
| `tsx` | MIT |
|
|
16
|
+
| `tweetnacl` | The Unlicense |
|
|
17
|
+
| `ws` | MIT |
|
|
18
|
+
| `zod` | MIT |
|
|
19
|
+
|
|
20
|
+
The unpublished Rust workspace also depends on separately licensed components,
|
|
21
|
+
including `mcport` (MIT), `blazingly-json` (MIT), `serde` (MIT OR Apache-2.0), and
|
|
22
|
+
BlindPlane crates (MIT OR Apache-2.0). Binary distributions must retain the
|
|
23
|
+
complete license texts required by the exact dependency versions they contain.
|
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
} from "../config";
|
|
26
26
|
import { recordAttributedCall } from "../mesh/call-scope";
|
|
27
27
|
import { classifyAction } from "../policy";
|
|
28
|
+
import {
|
|
29
|
+
evaluateEffectiveAction,
|
|
30
|
+
legacyGrantTapFlowAllowed,
|
|
31
|
+
} from "../policy/effective-action";
|
|
28
32
|
import { protectedGrantTapAccess } from "../self-protection";
|
|
29
33
|
|
|
30
34
|
async function readStdin(): Promise<string> {
|
|
@@ -35,31 +39,20 @@ async function readStdin(): Promise<string> {
|
|
|
35
39
|
|
|
36
40
|
async function main(): Promise<void> {
|
|
37
41
|
const raw = await readStdin();
|
|
38
|
-
let input: HookInput
|
|
42
|
+
let input: HookInput;
|
|
39
43
|
try {
|
|
40
44
|
input = JSON.parse(raw) as HookInput;
|
|
41
45
|
} catch {
|
|
42
|
-
|
|
43
|
-
JSON.stringify({
|
|
44
|
-
hookSpecificOutput: {
|
|
45
|
-
hookEventName: "PreToolUse",
|
|
46
|
-
permissionDecision: "deny",
|
|
47
|
-
permissionDecisionReason: "GrantTap received invalid hook JSON",
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
46
|
+
writePermission("deny", "GrantTap received invalid hook JSON");
|
|
51
47
|
return;
|
|
52
48
|
}
|
|
49
|
+
await handlePreToolUse(input);
|
|
50
|
+
}
|
|
53
51
|
|
|
52
|
+
async function handlePreToolUse(input: HookInput): Promise<void> {
|
|
54
53
|
const protectedAccess = protectedGrantTapAccess(input.tool_name, input.tool_input);
|
|
55
54
|
if (protectedAccess) {
|
|
56
|
-
|
|
57
|
-
hookSpecificOutput: {
|
|
58
|
-
hookEventName: "PreToolUse",
|
|
59
|
-
permissionDecision: "deny",
|
|
60
|
-
permissionDecisionReason: protectedAccess.reason,
|
|
61
|
-
},
|
|
62
|
-
}));
|
|
55
|
+
writePermission("deny", protectedAccess.reason);
|
|
63
56
|
return;
|
|
64
57
|
}
|
|
65
58
|
|
|
@@ -80,43 +73,42 @@ async function main(): Promise<void> {
|
|
|
80
73
|
input.tool_name,
|
|
81
74
|
input.tool_input,
|
|
82
75
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
76
|
+
const projectDecision = await evaluateEffectiveAction({
|
|
77
|
+
provider: "claude",
|
|
78
|
+
sessionId: input.session_id,
|
|
79
|
+
cwd: input.cwd,
|
|
80
|
+
toolName: input.tool_name,
|
|
81
|
+
toolInput: input.tool_input,
|
|
82
|
+
legacyDenyReason: blocked?.reason,
|
|
83
|
+
});
|
|
84
|
+
if (projectDecision.effect === "deny") {
|
|
85
|
+
writePermission("deny", projectDecision.reason);
|
|
93
86
|
return;
|
|
94
87
|
}
|
|
88
|
+
const legacyFlow = legacyGrantTapFlowAllowed(projectDecision);
|
|
95
89
|
|
|
96
90
|
// Claude already granted this chat unconditional tool access. GrantTap's
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
if (input.permission_mode === "bypassPermissions") return;
|
|
91
|
+
// Project ASK/DENY is a parent boundary, so bypass mode cannot weaken it.
|
|
92
|
+
if (legacyFlow && input.permission_mode === "bypassPermissions") return;
|
|
100
93
|
|
|
101
94
|
// Gating paused, or this session is exempt → abstain (empty output = Claude
|
|
102
95
|
// uses its normal permission flow, exactly as if GrantTap weren't installed).
|
|
103
|
-
if (isGatingSkipped(input.session_id)) return;
|
|
96
|
+
if (legacyFlow && isGatingSkipped(input.session_id)) return;
|
|
97
|
+
await continuePermissionFlow(input, legacyFlow);
|
|
98
|
+
}
|
|
104
99
|
|
|
100
|
+
async function continuePermissionFlow(input: HookInput, legacyFlow: boolean): Promise<void> {
|
|
105
101
|
let cfg;
|
|
106
102
|
try {
|
|
107
103
|
cfg = loadConfig(machineConfigPath());
|
|
108
104
|
} catch {
|
|
105
|
+
if (!legacyFlow) {
|
|
106
|
+
writePermission("deny", "Project policy requires GrantTap approval, but this computer is not paired");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
109
|
// No pairing yet: don't block the user's normal workflow — defer to Claude's
|
|
110
110
|
// own prompt by emitting "ask".
|
|
111
|
-
|
|
112
|
-
JSON.stringify({
|
|
113
|
-
hookSpecificOutput: {
|
|
114
|
-
hookEventName: "PreToolUse",
|
|
115
|
-
permissionDecision: "ask",
|
|
116
|
-
permissionDecisionReason: "GrantTap not paired (run `npm run init`)",
|
|
117
|
-
},
|
|
118
|
-
}),
|
|
119
|
-
);
|
|
111
|
+
writePermission("ask", "GrantTap not paired (run `npm run init`)");
|
|
120
112
|
return;
|
|
121
113
|
}
|
|
122
114
|
|
|
@@ -125,21 +117,8 @@ async function main(): Promise<void> {
|
|
|
125
117
|
// the user never finished setting up. Once paired, this is what keeps routine
|
|
126
118
|
// work independent of whether the phone is awake, reachable, or even running.
|
|
127
119
|
const req = claudeToRequest(input);
|
|
128
|
-
if (shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
|
|
129
|
-
|
|
130
|
-
const cls = classifyAction(req.tool, req.command);
|
|
131
|
-
process.stdout.write(
|
|
132
|
-
JSON.stringify(
|
|
133
|
-
decisionToClaudeOutput({
|
|
134
|
-
type: "approval.decision",
|
|
135
|
-
requestId: req.requestId,
|
|
136
|
-
decision: "allow",
|
|
137
|
-
decidedBy: "auto",
|
|
138
|
-
note: `GrantTap auto-accept (${level} / ${cls})`,
|
|
139
|
-
decidedAt: Date.now(),
|
|
140
|
-
}),
|
|
141
|
-
),
|
|
142
|
-
);
|
|
120
|
+
if (legacyFlow && shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
|
|
121
|
+
writeAutoApproval(input, req);
|
|
143
122
|
return;
|
|
144
123
|
}
|
|
145
124
|
|
|
@@ -153,12 +132,38 @@ async function main(): Promise<void> {
|
|
|
153
132
|
// Relay down or phone asleep ≠ phone said no: abstain so Claude's local flow
|
|
154
133
|
// handles it (avoid flooding "решай локально" on every tool).
|
|
155
134
|
if (isUnanswered(decision)) {
|
|
135
|
+
if (!legacyFlow) {
|
|
136
|
+
writePermission("deny", "Project approval was required but no decision was received");
|
|
137
|
+
}
|
|
156
138
|
return;
|
|
157
139
|
}
|
|
158
140
|
|
|
159
141
|
process.stdout.write(JSON.stringify(decisionToClaudeOutput(decision)));
|
|
160
142
|
}
|
|
161
143
|
|
|
144
|
+
function writeAutoApproval(input: HookInput, req: ReturnType<typeof claudeToRequest>): void {
|
|
145
|
+
const level = autoAcceptLevelFor(input.session_id);
|
|
146
|
+
const cls = classifyAction(req.tool, req.command);
|
|
147
|
+
process.stdout.write(JSON.stringify(decisionToClaudeOutput({
|
|
148
|
+
type: "approval.decision",
|
|
149
|
+
requestId: req.requestId,
|
|
150
|
+
decision: "allow",
|
|
151
|
+
decidedBy: "auto",
|
|
152
|
+
note: `GrantTap auto-accept (${level} / ${cls})`,
|
|
153
|
+
decidedAt: Date.now(),
|
|
154
|
+
})));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function writePermission(decision: "deny" | "ask", reason: string): void {
|
|
158
|
+
process.stdout.write(JSON.stringify({
|
|
159
|
+
hookSpecificOutput: {
|
|
160
|
+
hookEventName: "PreToolUse",
|
|
161
|
+
permissionDecision: decision,
|
|
162
|
+
permissionDecisionReason: reason,
|
|
163
|
+
},
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
162
167
|
main().catch((err) => {
|
|
163
168
|
process.stdout.write(
|
|
164
169
|
JSON.stringify({
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "../config";
|
|
26
26
|
import { recordAttributedCall } from "../mesh/call-scope";
|
|
27
27
|
import { classifyAction } from "../policy";
|
|
28
|
+
import { consumeCodexProjectAsk } from "../policy/codex-project-ask";
|
|
28
29
|
import { protectedGrantTapAccess } from "../self-protection";
|
|
29
30
|
|
|
30
31
|
async function readStdin(): Promise<string> {
|
|
@@ -77,9 +78,15 @@ async function main(): Promise<void> {
|
|
|
77
78
|
process.stdout.write(denyOutput(blocked.reason));
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
const projectAsk = consumeCodexProjectAsk({
|
|
82
|
+
sessionId: input.session_id,
|
|
83
|
+
toolUseId: input.tool_use_id,
|
|
84
|
+
toolName: input.tool_name,
|
|
85
|
+
toolInput: input.tool_input,
|
|
86
|
+
});
|
|
80
87
|
|
|
81
88
|
// Gating paused or this session exempt → stay silent (Codex uses its own flow).
|
|
82
|
-
if (isGatingSkipped(input.session_id)) return;
|
|
89
|
+
if (!projectAsk && isGatingSkipped(input.session_id)) return;
|
|
83
90
|
|
|
84
91
|
let cfg;
|
|
85
92
|
try {
|
|
@@ -91,7 +98,7 @@ async function main(): Promise<void> {
|
|
|
91
98
|
|
|
92
99
|
// Local policy, evaluated after pairing is confirmed — see claude-hook.
|
|
93
100
|
const req = codexToRequest(input);
|
|
94
|
-
if (shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
|
|
101
|
+
if (!projectAsk && shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
|
|
95
102
|
const level = autoAcceptLevelFor(input.session_id);
|
|
96
103
|
const cls = classifyAction(req.tool, req.command);
|
|
97
104
|
process.stdout.write(
|
|
@@ -116,9 +123,14 @@ async function main(): Promise<void> {
|
|
|
116
123
|
);
|
|
117
124
|
const decision = await requestApproval(cfg, req, { timeoutMs });
|
|
118
125
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
if (isUnanswered(decision))
|
|
126
|
+
// Routine relay failure stays in Codex's native flow. A Project ASK cannot
|
|
127
|
+
// fall through to provider approval because that would weaken the parent rule.
|
|
128
|
+
if (isUnanswered(decision)) {
|
|
129
|
+
if (projectAsk) {
|
|
130
|
+
process.stdout.write(denyOutput("Project approval was required but no decision was received"));
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
122
134
|
|
|
123
135
|
process.stdout.write(JSON.stringify(decisionToCodexOutput(decision)));
|
|
124
136
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
|
-
/**
|
|
2
|
+
/** Codex PreToolUse: deterministic deny or one-shot Project ASK handoff. */
|
|
3
3
|
import type { HookInput } from "../adapters";
|
|
4
4
|
import { blockedSessionCapability, isProviderEnabled } from "../config";
|
|
5
|
+
import { recordCodexProjectAsk } from "../policy/codex-project-ask";
|
|
6
|
+
import { evaluateEffectiveAction } from "../policy/effective-action";
|
|
5
7
|
import { protectedGrantTapAccess } from "../self-protection";
|
|
6
8
|
|
|
7
9
|
async function readStdin(): Promise<string> {
|
|
@@ -19,13 +21,7 @@ async function main(): Promise<void> {
|
|
|
19
21
|
}
|
|
20
22
|
const protectedAccess = protectedGrantTapAccess(input.tool_name, input.tool_input);
|
|
21
23
|
if (protectedAccess) {
|
|
22
|
-
|
|
23
|
-
hookSpecificOutput: {
|
|
24
|
-
hookEventName: "PreToolUse",
|
|
25
|
-
permissionDecision: "deny",
|
|
26
|
-
permissionDecisionReason: protectedAccess.reason,
|
|
27
|
-
},
|
|
28
|
-
}));
|
|
24
|
+
deny(protectedAccess.reason);
|
|
29
25
|
return;
|
|
30
26
|
}
|
|
31
27
|
if (!isProviderEnabled("codex")) return;
|
|
@@ -34,12 +30,36 @@ async function main(): Promise<void> {
|
|
|
34
30
|
input.tool_name,
|
|
35
31
|
input.tool_input,
|
|
36
32
|
);
|
|
37
|
-
|
|
33
|
+
const decision = await evaluateEffectiveAction({
|
|
34
|
+
provider: "codex",
|
|
35
|
+
sessionId: input.session_id,
|
|
36
|
+
cwd: input.cwd,
|
|
37
|
+
toolName: input.tool_name,
|
|
38
|
+
toolInput: input.tool_input,
|
|
39
|
+
legacyDenyReason: blocked?.reason,
|
|
40
|
+
});
|
|
41
|
+
if (decision.effect === "deny") {
|
|
42
|
+
deny(decision.reason);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (decision.effect !== "ask") return;
|
|
46
|
+
const recorded = recordCodexProjectAsk({
|
|
47
|
+
sessionId: input.session_id,
|
|
48
|
+
toolUseId: input.tool_use_id,
|
|
49
|
+
toolName: input.tool_name,
|
|
50
|
+
toolInput: input.tool_input,
|
|
51
|
+
}, decision.reason);
|
|
52
|
+
if (!recorded) {
|
|
53
|
+
deny("Project approval could not be bound to this exact Codex call");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function deny(reason: string): void {
|
|
38
58
|
process.stdout.write(JSON.stringify({
|
|
39
59
|
hookSpecificOutput: {
|
|
40
60
|
hookEventName: "PreToolUse",
|
|
41
61
|
permissionDecision: "deny",
|
|
42
|
-
permissionDecisionReason:
|
|
62
|
+
permissionDecisionReason: reason,
|
|
43
63
|
},
|
|
44
64
|
}));
|
|
45
65
|
}
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
machineConfigPath,
|
|
16
16
|
shouldAutoAcceptCursorShell,
|
|
17
17
|
} from "../config";
|
|
18
|
+
import {
|
|
19
|
+
evaluateEffectiveAction,
|
|
20
|
+
legacyGrantTapFlowAllowed,
|
|
21
|
+
} from "../policy/effective-action";
|
|
18
22
|
import { cursorRootSessionId } from "../sessions/cursor";
|
|
19
23
|
import { protectedGrantTapAccess } from "../self-protection";
|
|
20
24
|
|
|
@@ -48,6 +52,10 @@ async function main(): Promise<void> {
|
|
|
48
52
|
nativeAsk("GrantTap could not read this shell call; use Cursor approval.");
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
55
|
+
await handleShell(input);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleShell(input: CursorHookInput): Promise<void> {
|
|
51
59
|
const protectedAccess = protectedGrantTapAccess(
|
|
52
60
|
input.tool_name, input.tool_input, input.command,
|
|
53
61
|
);
|
|
@@ -66,19 +74,42 @@ async function main(): Promise<void> {
|
|
|
66
74
|
input.tool_name ?? "Shell",
|
|
67
75
|
input.tool_input ?? { command: input.command },
|
|
68
76
|
);
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
const projectDecision = await evaluateEffectiveAction({
|
|
78
|
+
provider: "cursor",
|
|
79
|
+
sessionId,
|
|
80
|
+
cwd: cursorCwd(input),
|
|
81
|
+
toolName: input.tool_name ?? "Shell",
|
|
82
|
+
toolInput: input.tool_input ?? { command: input.command },
|
|
83
|
+
legacyDenyReason: blocked?.reason,
|
|
84
|
+
});
|
|
85
|
+
if (projectDecision.effect === "deny") {
|
|
86
|
+
deny(projectDecision.reason);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const legacyFlow = legacyGrantTapFlowAllowed(projectDecision);
|
|
90
|
+
if (!sessionId) {
|
|
91
|
+
if (!legacyFlow) deny("Project approval cannot be bound to an exact Cursor chat");
|
|
92
|
+
else nativeAsk("GrantTap is paused or this shell call is unscoped; use Cursor approval.");
|
|
71
93
|
return;
|
|
72
94
|
}
|
|
73
|
-
if (
|
|
95
|
+
if (legacyFlow && isGatingSkipped(sessionId)) {
|
|
74
96
|
nativeAsk("GrantTap is paused or this shell call is unscoped; use Cursor approval.");
|
|
75
97
|
return;
|
|
76
98
|
}
|
|
99
|
+
await continueApproval(input, sessionId, legacyFlow);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function continueApproval(
|
|
103
|
+
input: CursorHookInput,
|
|
104
|
+
sessionId: string,
|
|
105
|
+
legacyFlow: boolean,
|
|
106
|
+
): Promise<void> {
|
|
77
107
|
let config;
|
|
78
108
|
try {
|
|
79
109
|
config = loadConfig(machineConfigPath());
|
|
80
110
|
} catch {
|
|
81
|
-
|
|
111
|
+
if (!legacyFlow) deny("Project policy requires GrantTap approval, but this computer is not paired");
|
|
112
|
+
else nativeAsk("GrantTap is not paired; use Cursor approval.");
|
|
82
113
|
return;
|
|
83
114
|
}
|
|
84
115
|
const request = cursorToRequest({
|
|
@@ -90,20 +121,35 @@ async function main(): Promise<void> {
|
|
|
90
121
|
// Eligible levels approve locally — never wait on phone WS/APNs for an action
|
|
91
122
|
// the configured level already allows.
|
|
92
123
|
const level = autoAcceptLevelFor(sessionId);
|
|
93
|
-
if (shouldAutoAcceptCursorShell(level, request.tool, request.command)) {
|
|
124
|
+
if (legacyFlow && shouldAutoAcceptCursorShell(level, request.tool, request.command)) {
|
|
94
125
|
process.stdout.write(JSON.stringify({ permission: "allow", continue: true }));
|
|
95
126
|
return;
|
|
96
127
|
}
|
|
97
128
|
|
|
98
129
|
const timeoutMs = Number(process.env.GRANTTAP_APPROVAL_TIMEOUT_MS ?? 60_000);
|
|
99
|
-
|
|
130
|
+
let decision;
|
|
131
|
+
try {
|
|
132
|
+
decision = await requestApproval(config, request, { timeoutMs });
|
|
133
|
+
} catch {
|
|
134
|
+
if (!legacyFlow) deny("Project approval failed before a decision was recorded");
|
|
135
|
+
else nativeAsk("GrantTap shell hook failed; use Cursor approval.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
100
138
|
if (isUnanswered(decision)) {
|
|
101
|
-
|
|
139
|
+
if (!legacyFlow) deny("Project approval was required but no decision was received");
|
|
140
|
+
else nativeAsk("GrantTap got no answer; use Cursor approval.");
|
|
102
141
|
return;
|
|
103
142
|
}
|
|
104
143
|
process.stdout.write(JSON.stringify(decisionToCursorOutput(decision)));
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
function cursorCwd(input: CursorHookInput): string | undefined {
|
|
147
|
+
if (typeof input.cwd === "string" && input.cwd.trim()) return input.cwd;
|
|
148
|
+
return Array.isArray(input.workspace_roots) && typeof input.workspace_roots[0] === "string"
|
|
149
|
+
? input.workspace_roots[0]
|
|
150
|
+
: undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
107
153
|
main().catch(() => {
|
|
108
154
|
nativeAsk("GrantTap shell hook failed; use Cursor approval.");
|
|
109
155
|
process.exit(0);
|