granttap-mcp 0.7.1 → 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.
Files changed (103) hide show
  1. package/LICENSE +86 -21
  2. package/README.md +43 -11
  3. package/SECURITY.md +33 -0
  4. package/THIRD_PARTY_NOTICES.md +23 -0
  5. package/apps/bridge/src/bin/claude-hook.ts +66 -48
  6. package/apps/bridge/src/bin/codex-hook.ts +24 -5
  7. package/apps/bridge/src/bin/codex-policy-hook.ts +36 -4
  8. package/apps/bridge/src/bin/cursor-hook.ts +69 -14
  9. package/apps/bridge/src/bin/cursor-mcp-hook.ts +104 -31
  10. package/apps/bridge/src/bin/monitor.ts +15 -1
  11. package/apps/bridge/src/bin/setup.ts +15 -1
  12. package/apps/bridge/src/capabilities/README.md +2 -0
  13. package/apps/bridge/src/config/README.md +3 -0
  14. package/apps/bridge/src/config/runtime.ts +26 -0
  15. package/apps/bridge/src/cursor-mcp-policy.ts +36 -1
  16. package/apps/bridge/src/engine/README.md +25 -0
  17. package/apps/bridge/src/engine/engine-client.ts +204 -0
  18. package/apps/bridge/src/engine/engine-declaration.ts +74 -0
  19. package/apps/bridge/src/engine/engine-health.ts +22 -0
  20. package/apps/bridge/src/engine/engine-policy-protocol.ts +262 -0
  21. package/apps/bridge/src/engine/engine-policy-types.ts +37 -0
  22. package/apps/bridge/src/engine/engine-projects.ts +68 -0
  23. package/apps/bridge/src/engine/engine-protocol.ts +265 -0
  24. package/apps/bridge/src/engine/engine-supervisor.ts +175 -0
  25. package/apps/bridge/src/install.ts +49 -25
  26. package/apps/bridge/src/launchd-safety.ts +53 -0
  27. package/apps/bridge/src/machine-load/README.md +10 -0
  28. package/apps/bridge/src/machine-load/agent-load-history.ts +94 -0
  29. package/apps/bridge/src/machine-load/index.ts +130 -0
  30. package/apps/bridge/src/machine-load/loop.ts +149 -0
  31. package/apps/bridge/src/machine-load/mcp-load-cache.ts +46 -0
  32. package/apps/bridge/src/machine-load/mcp-load-refresh.ts +65 -0
  33. package/apps/bridge/src/machine-load/mcp-process-sampler.ts +69 -0
  34. package/apps/bridge/src/machine-load/process-sampler.ts +99 -0
  35. package/apps/bridge/src/machine-load/scan-cost.ts +25 -0
  36. package/apps/bridge/src/mesh/README.md +29 -4
  37. package/apps/bridge/src/mesh/binding-state.ts +87 -0
  38. package/apps/bridge/src/mesh/call-scope.ts +1 -1
  39. package/apps/bridge/src/mesh/capsule.ts +2 -0
  40. package/apps/bridge/src/mesh/catalog.ts +111 -21
  41. package/apps/bridge/src/mesh/convergence.ts +84 -0
  42. package/apps/bridge/src/mesh/execution-sweep.ts +35 -0
  43. package/apps/bridge/src/mesh/identity.ts +21 -4
  44. package/apps/bridge/src/mesh/readiness.ts +19 -2
  45. package/apps/bridge/src/mesh/snapshot-merge.ts +29 -0
  46. package/apps/bridge/src/mesh/store-state.ts +90 -0
  47. package/apps/bridge/src/mesh/store-support.ts +15 -0
  48. package/apps/bridge/src/mesh/store.ts +104 -97
  49. package/apps/bridge/src/mesh/task-state.ts +106 -0
  50. package/apps/bridge/src/monitor-leadership.ts +139 -0
  51. package/apps/bridge/src/monitor.ts +36 -72
  52. package/apps/bridge/src/policy/README.md +23 -0
  53. package/apps/bridge/src/policy/capability-fingerprint.ts +211 -0
  54. package/apps/bridge/src/policy/codex-project-ask.ts +207 -0
  55. package/apps/bridge/src/policy/effective-action.ts +143 -0
  56. package/apps/bridge/src/project-policy/mapping.ts +156 -0
  57. package/apps/bridge/src/project-policy/runtime.ts +213 -0
  58. package/apps/bridge/src/reply/README.md +3 -0
  59. package/apps/bridge/src/reply/cursor-agent-bin.ts +46 -0
  60. package/apps/bridge/src/reply/provider-headless.ts +2 -1
  61. package/apps/bridge/src/reply/types.ts +3 -0
  62. package/apps/bridge/src/reply.ts +24 -5
  63. package/apps/bridge/src/self-protection.ts +149 -0
  64. package/apps/bridge/src/session-keys.ts +16 -1
  65. package/apps/bridge/src/sessions/activity-helpers.ts +2 -1
  66. package/apps/bridge/src/sessions/capability-totals.ts +0 -0
  67. package/apps/bridge/src/sessions/claude.ts +1 -1
  68. package/apps/bridge/src/sessions/codex-head.ts +77 -0
  69. package/apps/bridge/src/sessions/codex.ts +22 -24
  70. package/apps/bridge/src/sessions/cursor/README.md +3 -0
  71. package/apps/bridge/src/sessions/cursor/transcripts.ts +1 -1
  72. package/apps/bridge/src/sessions/telemetry/README.md +3 -0
  73. package/apps/bridge/src/sessions/telemetry/identity.ts +1 -0
  74. package/apps/bridge/src/sessions/telemetry.ts +17 -0
  75. package/apps/bridge/src/sessions.ts +17 -5
  76. package/apps/mcp/src/create-server.ts +1 -1
  77. package/apps/mcp/src/http-server.ts +3 -2
  78. package/apps/mcp/src/http-service/README.md +2 -0
  79. package/apps/mcp/src/http-service/installer.ts +3 -0
  80. package/apps/mcp/src/http-service/snapshot.ts +3 -2
  81. package/apps/mcp/src/mcp-tools/README.md +3 -0
  82. package/apps/mcp/src/mcp-tools/interaction.ts +26 -7
  83. package/apps/mcp/src/mcp-tools/relay.ts +29 -15
  84. package/apps/mcp/src/mesh-server.ts +1 -1
  85. package/apps/mcp/src/oauth/README.md +2 -0
  86. package/apps/mcp/src/oauth/loopback-origin.ts +16 -0
  87. package/bin/granttap-mcp.mjs +1 -1
  88. package/cursor-plugin/.cursor-plugin/plugin.json +1 -1
  89. package/cursor-plugin/LICENSE +86 -0
  90. package/cursor-plugin/README.md +5 -0
  91. package/docs/images/iphone-chat.png +0 -0
  92. package/docs/images/iphone-claude-tasks.png +0 -0
  93. package/docs/images/iphone-command-center.png +0 -0
  94. package/docs/images/iphone-mcp-usage.png +0 -0
  95. package/package.json +6 -5
  96. package/packages/protocol/messages/README.md +12 -0
  97. package/packages/protocol/messages/approvals.ts +3 -0
  98. package/packages/protocol/messages/capabilities.ts +54 -0
  99. package/packages/protocol/messages/interaction.ts +13 -0
  100. package/packages/protocol/messages/machine.ts +29 -0
  101. package/packages/protocol/messages/mesh.ts +31 -1
  102. package/packages/protocol/messages/project-policy.ts +172 -0
  103. package/packages/protocol/schema.ts +12 -0
package/LICENSE CHANGED
@@ -1,21 +1,86 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Serhii Ziborov
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
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
  [![npm](https://img.shields.io/npm/v/granttap-mcp)](https://www.npmjs.com/package/granttap-mcp)
4
4
  [![CI](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
5
- [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![License](https://img.shields.io/badge/license-GrantTap%20Commercial-7c3aed.svg)](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)
@@ -51,12 +55,11 @@ Mesh resource. Full transcripts and hidden reasoning are never mesh payloads.
51
55
  Claims have TTLs; a colliding claim is rejected before it is recorded so agents
52
56
  can choose different work or contact the owner before escalating to Needs You.
53
57
 
54
- Both directions are scoped to the calling execution. The provider hook already
55
- runs inside the agent's own session and sees the exact call, so GrantTap
56
- attributes every `notify` to the session that really made it and publishes the
57
- event only for that execution — a model that learned another session's id
58
- cannot act in its name. `granttap://mesh/current` therefore carries no Project
59
- data: an attributed call returns this execution's opaque
58
+ For Claude Code, Codex, and Cursor, the provider hook runs inside the agent's
59
+ own session and sees the exact call. GrantTap attributes every `notify` to the
60
+ session that really made it and publishes the event only for that execution —
61
+ a model that learned another session's id cannot act in its name.
62
+ `granttap://mesh/current` therefore carries no Project data: an attributed call returns this execution's opaque
60
63
  `granttap://mesh/<capability>` URI, and only that URI serves its Project, Task,
61
64
  owners, claims, dependencies, and relevant events. Ownership transfer stays out
62
65
  of the tool surface entirely: `HANDOFF_ACCEPTED` and `HANDOFF_REJECTED` are
@@ -66,8 +69,11 @@ The first executable handoff path is Claude Code ↔ Codex across linked
66
69
  computers. GrantTap builds a bounded Task Capsule from explicit task/git facts,
67
70
  requires local phone authorization, creates a separate target branch/worktree,
68
71
  starts the target execution, and returns a receipt bound to the exact capsule.
69
- Cursor and Grok Build use the same provider-neutral schema and discovery model;
70
- unsupported remote-start paths fail closed instead of claiming parity.
72
+ Cursor uses the same provider-neutral schema and trusted caller attribution.
73
+ Grok Build remains Experimental and observable where available, but does not
74
+ yet expose a trusted caller hook, so agent-authored scoped Mesh events are not
75
+ offered for it. Unsupported remote-start paths fail closed instead of claiming
76
+ parity.
71
77
 
72
78
  A capsule carries facts, not files. A handoff from a checkout with uncommitted
73
79
  changes is refused — "This task has uncommitted changes. Commit or checkpoint
@@ -95,6 +101,9 @@ history stays on the device.
95
101
 
96
102
  ## Install
97
103
 
104
+ Installation and production use require Authorized Access under the
105
+ [GrantTap Commercial Source License](LICENSE).
106
+
98
107
  ```bash
99
108
  npm install -g granttap-mcp
100
109
  granttap setup
@@ -105,10 +114,17 @@ hooks, installs the background helper, configures Cursor's persistent local
105
114
  OAuth service when Cursor is present, and starts phone pairing when run in an
106
115
  interactive terminal. It ends with one exact next action.
107
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
+
108
124
  The normal CLI surface is intentionally small:
109
125
 
110
126
  ```text
111
- granttap setup
127
+ granttap setup [--engine <path>]
112
128
  granttap status [--json]
113
129
  granttap connect [--relay <wss-url>]
114
130
  granttap reset [--yes]
@@ -160,11 +176,19 @@ The bounded encrypted protocol preserves:
160
176
  - visible activity, delivery state, context and token counters;
161
177
  - MCP, Skill, and CLI observations;
162
178
  - child-agent relationships;
163
- - 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.
164
181
 
165
182
  An optional bounded `errorClass` may describe an error category. Full tool
166
183
  error payloads are not copied into usage telemetry by default.
167
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
+
168
192
  ## Local enforcement
169
193
 
170
194
  GrantTap can narrow later actions for an exact task only where a provider
@@ -215,4 +239,12 @@ gate on macOS, and `npm publish` enforces the same gate through
215
239
  Do not publish from a dirty checkout or before the package allowlist, tests,
216
240
  typecheck, and release checks pass.
217
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
+
218
250
  GrantTap is not affiliated with Anthropic, OpenAI, Apple, Anysphere, or xAI.
package/SECURITY.md CHANGED
@@ -20,6 +20,24 @@ credential used for authenticated WebSocket and APNs registration.
20
20
  Provider credentials remain in each provider's own local authentication flow.
21
21
  GrantTap does not send them to the relay.
22
22
 
23
+ Installed Claude Code, Codex, and Cursor hooks deny direct agent tool calls
24
+ that name GrantTap's local pairing, key, or policy files, including calls that
25
+ would otherwise be auto-accepted or run in provider bypass mode. The denial
26
+ covers the config directory itself, any glob over it, and every file not on a
27
+ short access-mode list, so a file added later is protected the day it appears.
28
+ Only helper logs and bounded runtime diagnostics are agent-readable, and they
29
+ remain read-only. Raw Mesh state and hook-attribution records are never exposed
30
+ to agent tools; `workspaces/` and `worktrees/` remain normal editable agent
31
+ workspace. The guard denies only what it positively recognised as a path into
32
+ that directory, and a fault inside the guard itself is printed and allowed
33
+ through instead of blocking every tool call on the machine: an outage of every
34
+ agent is a worse failure than the file it was watching. This is
35
+ best-effort self-protection, not an operating-system security boundary: a
36
+ process running as the same macOS user can use an unobserved tool, shell
37
+ obfuscation, or direct filesystem access outside those hooks. Protect the user
38
+ account and disk; use a trusted terminal for intentional maintenance. Moving
39
+ remaining local secret material into Keychain is the stronger future boundary.
40
+
23
41
  ## Cryptographic boundary
24
42
 
25
43
  - Every computer pairing is generated locally with unique Curve25519 keys.
@@ -67,6 +85,10 @@ acceptance includes a SHA-256 receipt over a canonical capsule and the source,
67
85
  target, Task identity, and acceptance time. Stale claims expire, replayed event
68
86
  IDs are ignored, and destination routing is explicit.
69
87
 
88
+ Repository remotes included in Project snapshots are reduced to lowercase
89
+ host/path form. Userinfo, passwords or tokens, query strings, fragments, and
90
+ local-path remotes are never persisted as `baseRemote`.
91
+
70
92
  ## Runtime controls
71
93
 
72
94
  Provider hooks enforce policy on the computer. Global provider deny always
@@ -87,5 +109,16 @@ expiry, actor, Project scope, and operation, and a revoked or disabled endpoint
87
109
  fails closed. Invite creation, actor enablement, Project scope, and revocation
88
110
  stay in the iPhone UI and the trusted CLI, never in a model-callable tool.
89
111
 
112
+ ## Local helper installation
113
+
114
+ The background helper is a user LaunchAgent, and `launchctl bootstrap` replaces
115
+ whatever job already holds that label. GrantTap therefore refuses to load a
116
+ LaunchAgent whose plist lives under the operating system's temporary directory:
117
+ a test, probe, or agent run with a temporary `HOME` must never evict the
118
+ installed helper and leave launchd restarting a program whose files were already
119
+ cleaned up. Only `GRANTTAP_TEST_FAKE_LAUNCHCTL=1`, which states that `launchctl`
120
+ on `PATH` is a stub, lifts that refusal.
121
+
122
+
90
123
  Relay vulnerabilities should be reported through the
91
124
  [GrantTap relay advisory form](https://github.com/sergii-ziborov/granttap-relay/security/advisories/new).
@@ -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,11 @@ 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";
32
+ import { protectedGrantTapAccess } from "../self-protection";
28
33
 
29
34
  async function readStdin(): Promise<string> {
30
35
  const chunks: Buffer[] = [];
@@ -34,19 +39,20 @@ async function readStdin(): Promise<string> {
34
39
 
35
40
  async function main(): Promise<void> {
36
41
  const raw = await readStdin();
37
- let input: HookInput = {};
42
+ let input: HookInput;
38
43
  try {
39
44
  input = JSON.parse(raw) as HookInput;
40
45
  } catch {
41
- process.stdout.write(
42
- JSON.stringify({
43
- hookSpecificOutput: {
44
- hookEventName: "PreToolUse",
45
- permissionDecision: "deny",
46
- permissionDecisionReason: "GrantTap received invalid hook JSON",
47
- },
48
- }),
49
- );
46
+ writePermission("deny", "GrantTap received invalid hook JSON");
47
+ return;
48
+ }
49
+ await handlePreToolUse(input);
50
+ }
51
+
52
+ async function handlePreToolUse(input: HookInput): Promise<void> {
53
+ const protectedAccess = protectedGrantTapAccess(input.tool_name, input.tool_input);
54
+ if (protectedAccess) {
55
+ writePermission("deny", protectedAccess.reason);
50
56
  return;
51
57
  }
52
58
 
@@ -67,43 +73,42 @@ async function main(): Promise<void> {
67
73
  input.tool_name,
68
74
  input.tool_input,
69
75
  );
70
- if (blocked) {
71
- process.stdout.write(
72
- JSON.stringify({
73
- hookSpecificOutput: {
74
- hookEventName: "PreToolUse",
75
- permissionDecision: "deny",
76
- permissionDecisionReason: blocked.reason,
77
- },
78
- }),
79
- );
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);
80
86
  return;
81
87
  }
88
+ const legacyFlow = legacyGrantTapFlowAllowed(projectDecision);
82
89
 
83
90
  // Claude already granted this chat unconditional tool access. GrantTap's
84
- // explicit MCP/skill/CLI blocks above still win, but phone approval must not
85
- // re-prompt a call Claude is intentionally running in bypass mode.
86
- 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;
87
93
 
88
94
  // Gating paused, or this session is exempt → abstain (empty output = Claude
89
95
  // uses its normal permission flow, exactly as if GrantTap weren't installed).
90
- if (isGatingSkipped(input.session_id)) return;
96
+ if (legacyFlow && isGatingSkipped(input.session_id)) return;
97
+ await continuePermissionFlow(input, legacyFlow);
98
+ }
91
99
 
100
+ async function continuePermissionFlow(input: HookInput, legacyFlow: boolean): Promise<void> {
92
101
  let cfg;
93
102
  try {
94
103
  cfg = loadConfig(machineConfigPath());
95
104
  } catch {
105
+ if (!legacyFlow) {
106
+ writePermission("deny", "Project policy requires GrantTap approval, but this computer is not paired");
107
+ return;
108
+ }
96
109
  // No pairing yet: don't block the user's normal workflow — defer to Claude's
97
110
  // own prompt by emitting "ask".
98
- process.stdout.write(
99
- JSON.stringify({
100
- hookSpecificOutput: {
101
- hookEventName: "PreToolUse",
102
- permissionDecision: "ask",
103
- permissionDecisionReason: "GrantTap not paired (run `npm run init`)",
104
- },
105
- }),
106
- );
111
+ writePermission("ask", "GrantTap not paired (run `npm run init`)");
107
112
  return;
108
113
  }
109
114
 
@@ -112,21 +117,8 @@ async function main(): Promise<void> {
112
117
  // the user never finished setting up. Once paired, this is what keeps routine
113
118
  // work independent of whether the phone is awake, reachable, or even running.
114
119
  const req = claudeToRequest(input);
115
- if (shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
116
- const level = autoAcceptLevelFor(input.session_id);
117
- const cls = classifyAction(req.tool, req.command);
118
- process.stdout.write(
119
- JSON.stringify(
120
- decisionToClaudeOutput({
121
- type: "approval.decision",
122
- requestId: req.requestId,
123
- decision: "allow",
124
- decidedBy: "auto",
125
- note: `GrantTap auto-accept (${level} / ${cls})`,
126
- decidedAt: Date.now(),
127
- }),
128
- ),
129
- );
120
+ if (legacyFlow && shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
121
+ writeAutoApproval(input, req);
130
122
  return;
131
123
  }
132
124
 
@@ -140,12 +132,38 @@ async function main(): Promise<void> {
140
132
  // Relay down or phone asleep ≠ phone said no: abstain so Claude's local flow
141
133
  // handles it (avoid flooding "решай локально" on every tool).
142
134
  if (isUnanswered(decision)) {
135
+ if (!legacyFlow) {
136
+ writePermission("deny", "Project approval was required but no decision was received");
137
+ }
143
138
  return;
144
139
  }
145
140
 
146
141
  process.stdout.write(JSON.stringify(decisionToClaudeOutput(decision)));
147
142
  }
148
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
+
149
167
  main().catch((err) => {
150
168
  process.stdout.write(
151
169
  JSON.stringify({
@@ -25,6 +25,8 @@ 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";
29
+ import { protectedGrantTapAccess } from "../self-protection";
28
30
 
29
31
  async function readStdin(): Promise<string> {
30
32
  const chunks: Buffer[] = [];
@@ -51,6 +53,12 @@ async function main(): Promise<void> {
51
53
  return;
52
54
  }
53
55
 
56
+ const protectedAccess = protectedGrantTapAccess(input.tool_name, input.tool_input);
57
+ if (protectedAccess) {
58
+ process.stdout.write(denyOutput(protectedAccess.reason));
59
+ return;
60
+ }
61
+
54
62
  if (!isProviderEnabled("codex")) return;
55
63
 
56
64
  // See claude-hook.ts: the MCP server trusts this attribution, never the model.
@@ -70,9 +78,15 @@ async function main(): Promise<void> {
70
78
  process.stdout.write(denyOutput(blocked.reason));
71
79
  return;
72
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
+ });
73
87
 
74
88
  // Gating paused or this session exempt → stay silent (Codex uses its own flow).
75
- if (isGatingSkipped(input.session_id)) return;
89
+ if (!projectAsk && isGatingSkipped(input.session_id)) return;
76
90
 
77
91
  let cfg;
78
92
  try {
@@ -84,7 +98,7 @@ async function main(): Promise<void> {
84
98
 
85
99
  // Local policy, evaluated after pairing is confirmed — see claude-hook.
86
100
  const req = codexToRequest(input);
87
- if (shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
101
+ if (!projectAsk && shouldAutoAcceptTool(input.session_id, req.tool, req.command)) {
88
102
  const level = autoAcceptLevelFor(input.session_id);
89
103
  const cls = classifyAction(req.tool, req.command);
90
104
  process.stdout.write(
@@ -109,9 +123,14 @@ async function main(): Promise<void> {
109
123
  );
110
124
  const decision = await requestApproval(cfg, req, { timeoutMs });
111
125
 
112
- // Relay down or phone never answered: stay silent an empty hook result
113
- // sends Codex back to its own approval flow, so desk work keeps going.
114
- if (isUnanswered(decision)) return;
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
+ }
115
134
 
116
135
  process.stdout.write(JSON.stringify(decisionToCodexOutput(decision)));
117
136
  }
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env -S npx tsx
2
- /** Deny-only Codex PreToolUse policy for exact per-chat capability switches. */
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";
7
+ import { protectedGrantTapAccess } from "../self-protection";
5
8
 
6
9
  async function readStdin(): Promise<string> {
7
10
  const chunks: Buffer[] = [];
@@ -10,24 +13,53 @@ async function readStdin(): Promise<string> {
10
13
  }
11
14
 
12
15
  async function main(): Promise<void> {
13
- if (!isProviderEnabled("codex")) return;
14
16
  let input: HookInput;
15
17
  try {
16
18
  input = JSON.parse(await readStdin()) as HookInput;
17
19
  } catch {
18
20
  return;
19
21
  }
22
+ const protectedAccess = protectedGrantTapAccess(input.tool_name, input.tool_input);
23
+ if (protectedAccess) {
24
+ deny(protectedAccess.reason);
25
+ return;
26
+ }
27
+ if (!isProviderEnabled("codex")) return;
20
28
  const blocked = blockedSessionCapability(
21
29
  input.session_id,
22
30
  input.tool_name,
23
31
  input.tool_input,
24
32
  );
25
- if (!blocked) return;
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 {
26
58
  process.stdout.write(JSON.stringify({
27
59
  hookSpecificOutput: {
28
60
  hookEventName: "PreToolUse",
29
61
  permissionDecision: "deny",
30
- permissionDecisionReason: blocked.reason,
62
+ permissionDecisionReason: reason,
31
63
  },
32
64
  }));
33
65
  }