granttap-mcp 0.8.32 → 0.8.34
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 +13 -10
- 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
|
@@ -381,19 +381,20 @@ trust.
|
|
|
381
381
|
|
|
382
382
|
## MCP contract
|
|
383
383
|
|
|
384
|
-
`tools/list` returns exactly
|
|
384
|
+
`tools/list` returns exactly six public tools:
|
|
385
385
|
|
|
386
386
|
| Tool | Contract |
|
|
387
387
|
| --- | --- |
|
|
388
|
+
| `connection_status` | Open the connection card and inspect saved pairing, QR expiry, relay observations, and provider readiness without changing pairing |
|
|
388
389
|
| `connect` | Reuse the existing production pairing or return a one-time QR |
|
|
389
|
-
| `reconnect` |
|
|
390
|
+
| `reconnect` | After explicit confirmation, show a one-time QR to reconnect or add an independent controller device while retaining the current room |
|
|
390
391
|
| `notify` | Send a non-blocking status update of at most 2,000 characters |
|
|
391
392
|
| `ask_yes_no` | Ask a yes/no question and wait for the explicit answer |
|
|
392
393
|
| `ask` | Ask an open question and wait for typed or spoken text |
|
|
393
394
|
|
|
394
395
|
MCP `connect` accepts no custom routing, replacement, or key-rotation input.
|
|
395
|
-
`reconnect` is declared destructive and requires `confirmed: true`;
|
|
396
|
-
|
|
396
|
+
`reconnect` is declared destructive and requires `confirmed: true`; the
|
|
397
|
+
existing pairing remains usable during a new enrollment attempt.
|
|
397
398
|
Setup is CLI-only because it changes provider configuration and must not be
|
|
398
399
|
available to a model through prompt injection.
|
|
399
400
|
|
|
@@ -509,12 +510,14 @@ is left to a trusted terminal, with the command spelled out in the answer. The
|
|
|
509
510
|
result — version before and after, the updater's own output — comes back as
|
|
510
511
|
`tool.update.result`.
|
|
511
512
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
513
|
+
GrantTap derives usage from native provider transcripts and local OS samples;
|
|
514
|
+
it does not ask an agent to write a resource report. An MCP server can be
|
|
515
|
+
sampled directly while it runs. For a completed CLI call, a unique observed
|
|
516
|
+
process tree yields measured CPU time and peak RSS. If only agent-wide samples
|
|
517
|
+
cover a built-in tool call, GrantTap labels its divided share as attributed.
|
|
518
|
+
Ambiguous, unsampled, and historical calls remain unknown rather than borrowing
|
|
519
|
+
a number from another moment. Estimated context tokens are labeled separately
|
|
520
|
+
from provider-reported model tokens.
|
|
518
521
|
|
|
519
522
|
## Local enforcement
|
|
520
523
|
|
|
@@ -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.34
|
|
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.34\";\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.34";
|
|
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) => {
|