granttap-mcp 0.8.32 → 0.8.33
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 +8 -6
- package/apps/bridge/src/account-recovery/README.md +6 -2
- package/apps/bridge/src/account-recovery/phone-link.ts +28 -0
- package/apps/bridge/src/monitor/handlers/inbound.ts +16 -2
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/protocol/messages/device/account-machine-link.ts +11 -0
- package/packages/protocol/schema.ts +3 -0
package/README.md
CHANGED
|
@@ -509,12 +509,14 @@ is left to a trusted terminal, with the command spelled out in the answer. The
|
|
|
509
509
|
result — version before and after, the updater's own output — comes back as
|
|
510
510
|
`tool.update.result`.
|
|
511
511
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
512
|
+
GrantTap derives usage from native provider transcripts and local OS samples;
|
|
513
|
+
it does not ask an agent to write a resource report. An MCP server can be
|
|
514
|
+
sampled directly while it runs. For a completed CLI call, a unique observed
|
|
515
|
+
process tree yields measured CPU time and peak RSS. If only agent-wide samples
|
|
516
|
+
cover a built-in tool call, GrantTap labels its divided share as attributed.
|
|
517
|
+
Ambiguous, unsampled, and historical calls remain unknown rather than borrowing
|
|
518
|
+
a number from another moment. Estimated context tokens are labeled separately
|
|
519
|
+
from provider-reported model tokens.
|
|
518
520
|
|
|
519
521
|
## Local enforcement
|
|
520
522
|
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`link.ts` persists the local Mac's opt-in account machine credential in its
|
|
4
4
|
private GrantTap config directory. The Mac app can enroll through its existing
|
|
5
|
-
authorized loopback connection
|
|
6
|
-
|
|
5
|
+
authorized loopback connection; the provider MCP consent flow can enroll after
|
|
6
|
+
a verified passkey assertion. `phone-link.ts` lets a QR-paired phone grant a
|
|
7
|
+
machine-scoped credential through the existing encrypted device room after
|
|
8
|
+
passkey sign-in. The Mac verifies it against the account service before saving
|
|
9
|
+
and never receives the phone's account session token. The credential lets
|
|
10
|
+
`poller.ts` answer
|
|
7
11
|
short-lived, account-authorized phone requests. `offer.ts` creates a new phone
|
|
8
12
|
controller key in the current room and seals its pairing half to the phone's
|
|
9
13
|
ephemeral public key. The website stores only the machine token hash and sealed
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AccountMachineLink } from "../../../../packages/protocol/schema";
|
|
2
|
+
import { loadAccountLink, saveAccountLink } from "./link";
|
|
3
|
+
|
|
4
|
+
/** A trusted QR room can grant only a machine credential, never an account session. */
|
|
5
|
+
export async function acceptPhoneAccountLink(
|
|
6
|
+
link: AccountMachineLink, origin: string, request: typeof fetch = fetch,
|
|
7
|
+
): Promise<boolean> {
|
|
8
|
+
if (new URL(origin).protocol !== "https:") return false;
|
|
9
|
+
const current = loadAccountLink();
|
|
10
|
+
if (current && current.accountId !== link.accountId) return false;
|
|
11
|
+
const response = await request(`${origin}/api/account/machine/identity`, {
|
|
12
|
+
headers: { authorization: `Bearer ${link.machineToken}`, accept: "application/json" },
|
|
13
|
+
signal: AbortSignal.timeout(8_000), redirect: "error",
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) return false;
|
|
16
|
+
const identity = await response.json() as { accountId?: string; machineId?: string };
|
|
17
|
+
if (identity.accountId !== link.accountId || identity.machineId !== link.machineId) return false;
|
|
18
|
+
if (current && current.machineId !== link.machineId) {
|
|
19
|
+
const revoked = await request(`${origin}/api/account/machine/self`, {
|
|
20
|
+
method: "DELETE",
|
|
21
|
+
headers: { authorization: `Bearer ${current.machineToken}`, accept: "application/json" },
|
|
22
|
+
signal: AbortSignal.timeout(8_000), redirect: "error",
|
|
23
|
+
});
|
|
24
|
+
if (!revoked.ok && revoked.status !== 401) return false;
|
|
25
|
+
}
|
|
26
|
+
return saveAccountLink({ accountId: link.accountId,
|
|
27
|
+
machineId: link.machineId, machineToken: link.machineToken });
|
|
28
|
+
}
|
|
@@ -36,6 +36,7 @@ import { publishSessionEvents } from "./catalog";
|
|
|
36
36
|
import { publishHistoryPage } from "../history/pages";
|
|
37
37
|
import { requestProjectGraphRefresh } from "../graph-refresh";
|
|
38
38
|
import { handleControllerPairRequest, handleInboundKnowledgeWrite } from "../controller-knowledge";
|
|
39
|
+
import { acceptPhoneAccountLink } from "../../account-recovery/phone-link";
|
|
39
40
|
|
|
40
41
|
export async function handleMonitorMessage(
|
|
41
42
|
client: RelayClient,
|
|
@@ -55,8 +56,8 @@ export async function handleMonitorMessage(
|
|
|
55
56
|
// Codex may start one MCP server per open task. Exactly one instance owns
|
|
56
57
|
// phone routing, so a single phone message can never create duplicate tasks.
|
|
57
58
|
if (!leadership.acquire()) return false;
|
|
58
|
-
if (payload.type === "controller.pair.request") {
|
|
59
|
-
return
|
|
59
|
+
if (payload.type === "controller.pair.request" || payload.type === "account.machine.link") {
|
|
60
|
+
return handleDeviceEnrollment(client, payload, input.peerPublicKey);
|
|
60
61
|
}
|
|
61
62
|
if (payload.type === "provider.hook.trust") {
|
|
62
63
|
return handleHookTrust(client, payload, input.peerPublicKey, publish);
|
|
@@ -138,6 +139,19 @@ export async function handleMonitorMessage(
|
|
|
138
139
|
return false;
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
function handleDeviceEnrollment(
|
|
143
|
+
client: RelayClient,
|
|
144
|
+
payload: Extract<Payload, { type: "account.machine.link" | "controller.pair.request" }>,
|
|
145
|
+
peerPublicKey?: string,
|
|
146
|
+
): Promise<boolean> {
|
|
147
|
+
if (payload.type === "controller.pair.request") {
|
|
148
|
+
return handleControllerPairRequest(client, payload, peerPublicKey);
|
|
149
|
+
}
|
|
150
|
+
if (!peerPublicKey) return Promise.resolve(false);
|
|
151
|
+
const origin = process.env.GRANTTAP_WEBSITE_ORIGIN || "https://granttap.com";
|
|
152
|
+
return acceptPhoneAccountLink(payload, origin).catch(() => false);
|
|
153
|
+
}
|
|
154
|
+
|
|
141
155
|
async function handleInboundClaimRelease(
|
|
142
156
|
client: RelayClient, payload: Extract<Payload, { type: "mesh.claim.release" }>,
|
|
143
157
|
publish: (forceHistory?: boolean) => Promise<void>,
|
package/cursor-plugin/README.md
CHANGED
|
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
|
|
|
10
10
|
Install the supported CLI on this computer, then run:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g granttap-mcp@0.8.
|
|
13
|
+
npm install -g granttap-mcp@0.8.33
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": [
|
|
6
6
|
"-e",
|
|
7
|
-
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.
|
|
7
|
+
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.33\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
|
|
|
7
7
|
const { existsSync } = require("node:fs");
|
|
8
8
|
const { delimiter, dirname, join } = require("node:path");
|
|
9
9
|
|
|
10
|
-
const PACKAGE = "granttap-mcp@0.8.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.33";
|
|
11
11
|
const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
|
|
12
12
|
const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
|
|
13
13
|
? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** A QR-paired phone grants this computer a machine-scoped account credential. */
|
|
4
|
+
export const AccountMachineLink = z.object({
|
|
5
|
+
type: z.literal("account.machine.link"),
|
|
6
|
+
accountId: z.string().uuid(),
|
|
7
|
+
machineId: z.string().uuid(),
|
|
8
|
+
machineToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
9
|
+
createdAt: z.number().nonnegative(),
|
|
10
|
+
}).strict();
|
|
11
|
+
export type AccountMachineLink = z.infer<typeof AccountMachineLink>;
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { MachineLoad } from "./messages/machine";
|
|
34
34
|
import { PairingJoin } from "./messages/pairing-join";
|
|
35
35
|
import { ControllerPairOffer, ControllerPairRequest } from "./messages/device/controller-pairing";
|
|
36
|
+
import { AccountMachineLink } from "./messages/device/account-machine-link";
|
|
36
37
|
import { KnowledgeWrite, KnowledgeWriteResult } from "./messages/mesh/knowledge/write";
|
|
37
38
|
import { MeshInvocationPage, MeshInvocationQuery } from "./messages/mesh/invocations";
|
|
38
39
|
import {
|
|
@@ -68,6 +69,7 @@ export * from "./messages/interaction";
|
|
|
68
69
|
export * from "./messages/machine";
|
|
69
70
|
export * from "./messages/pairing-join";
|
|
70
71
|
export * from "./messages/device/controller-pairing";
|
|
72
|
+
export * from "./messages/device/account-machine-link";
|
|
71
73
|
export * from "./messages/mesh/knowledge/write";
|
|
72
74
|
export * from "./messages/mesh";
|
|
73
75
|
export * from "./messages/mesh/invocations";
|
|
@@ -157,6 +159,7 @@ export const Payload = z.union([
|
|
|
157
159
|
PairingJoin,
|
|
158
160
|
ControllerPairRequest,
|
|
159
161
|
ControllerPairOffer,
|
|
162
|
+
AccountMachineLink,
|
|
160
163
|
KnowledgeWrite,
|
|
161
164
|
KnowledgeWriteResult,
|
|
162
165
|
]).superRefine((payload, ctx) => {
|