granttap-mcp 0.7.0 → 0.7.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/README.md CHANGED
@@ -45,12 +45,22 @@ Project Mesh adds stable Project and Task identity above provider-native
45
45
  sessions. A Task can retain its `taskId` across multiple executions, agents,
46
46
  and computers while each native session remains intact.
47
47
 
48
- Agents can read the compact `granttap://mesh/current` MCP resource and publish
49
- bounded progress, dependency, question, answer, claim, conflict, and completion
50
- events through the existing `notify` tool. Full transcripts and hidden
51
- reasoning are never mesh payloads. Claims have TTLs; a colliding claim is
52
- rejected before it is recorded so agents can choose different work or contact
53
- the owner before escalating to Needs You.
48
+ Agents publish bounded progress, dependency, question, answer, claim, conflict,
49
+ and completion events through the existing `notify` tool, and read state from a
50
+ Mesh resource. Full transcripts and hidden reasoning are never mesh payloads.
51
+ Claims have TTLs; a colliding claim is rejected before it is recorded so agents
52
+ can choose different work or contact the owner before escalating to Needs You.
53
+
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
60
+ `granttap://mesh/<capability>` URI, and only that URI serves its Project, Task,
61
+ owners, claims, dependencies, and relevant events. Ownership transfer stays out
62
+ of the tool surface entirely: `HANDOFF_ACCEPTED` and `HANDOFF_REJECTED` are
63
+ decided by the runtime after receipt verification.
54
64
 
55
65
  The first executable handoff path is Claude Code ↔ Codex across linked
56
66
  computers. GrantTap builds a bounded Task Capsule from explicit task/git facts,
@@ -59,6 +69,14 @@ starts the target execution, and returns a receipt bound to the exact capsule.
59
69
  Cursor and Grok Build use the same provider-neutral schema and discovery model;
60
70
  unsupported remote-start paths fail closed instead of claiming parity.
61
71
 
72
+ A capsule carries facts, not files. A handoff from a checkout with uncommitted
73
+ changes is refused — "This task has uncommitted changes. Commit or checkpoint
74
+ them before moving the task." — instead of silently continuing the Task from
75
+ committed state and leaving that work behind. The destination is refused just
76
+ as explicitly when the named commit is not on its computer, or when the
77
+ capsule's own resource claims overlap another execution's; GrantTap never
78
+ pushes or fetches on its own.
79
+
62
80
  ### Grok Bot as a scoped Mesh participant
63
81
 
64
82
  Grok Bot is a persistent agent, not a coding-agent integration. The iPhone
@@ -127,7 +145,8 @@ Setup is CLI-only because it changes provider configuration and must not be
127
145
  available to a model through prompt injection.
128
146
 
129
147
  `notify` may alternatively carry one bounded task-scoped Mesh event. This does
130
- not add a fifth MCP tool or grant any global setup capability.
148
+ not add a fifth MCP tool or grant any global setup capability, and it publishes
149
+ only for the execution whose provider hook attributed the call.
131
150
 
132
151
  Provider-native approvals and mobile continuation require the matching local
133
152
  adapter. MCP registration alone is never reported as proof that an integration
package/SECURITY.md CHANGED
@@ -49,6 +49,16 @@ whose independent keys were granted to it. The protection is against relay,
49
49
  database/network compromise, other pairings, and disclosure of a different
50
50
  task key—not against an endpoint already authorized for that task.
51
51
 
52
+ Project Mesh reads and writes are scoped to one execution. The MCP server
53
+ cannot see which agent called it, so the provider hook — which runs inside the
54
+ session and receives the exact arguments — records a single-use attribution for
55
+ that call, and the runtime publishes the event for that session only. A model
56
+ that learned another execution's session id, Project, or Task cannot publish in
57
+ its name, and the unscoped `granttap://mesh/current` URI carries no Project
58
+ data at all. The opaque capability returned to an attributed caller names one
59
+ Project, Task, session and provider, expires, and never authorizes ownership
60
+ transfer.
61
+
52
62
  Task Capsules are strict, bounded schemas containing goal, explicit status,
53
63
  repository/commit facts, changed-file names, tests, dependencies, claims,
54
64
  remaining work, and explicit decisions. Unknown fields are rejected; hidden
@@ -23,6 +23,7 @@ import {
23
23
  machineConfigPath,
24
24
  shouldAutoAcceptTool,
25
25
  } from "../config";
26
+ import { recordAttributedCall } from "../mesh/call-scope";
26
27
  import { classifyAction } from "../policy";
27
28
 
28
29
  async function readStdin(): Promise<string> {
@@ -51,6 +52,16 @@ async function main(): Promise<void> {
51
52
 
52
53
  if (!isProviderEnabled("claude")) return;
53
54
 
55
+ // Attribute this exact GrantTap call to the session that made it. The MCP
56
+ // server cannot see the caller, so without this a model could publish Mesh
57
+ // events in another execution's name.
58
+ recordAttributedCall({
59
+ provider: "claude",
60
+ sessionId: input.session_id,
61
+ toolName: input.tool_name,
62
+ args: input.tool_input,
63
+ });
64
+
54
65
  const blocked = blockedSessionCapability(
55
66
  input.session_id,
56
67
  input.tool_name,
@@ -23,6 +23,7 @@ import {
23
23
  machineConfigPath,
24
24
  shouldAutoAcceptTool,
25
25
  } from "../config";
26
+ import { recordAttributedCall } from "../mesh/call-scope";
26
27
  import { classifyAction } from "../policy";
27
28
 
28
29
  async function readStdin(): Promise<string> {
@@ -52,6 +53,14 @@ async function main(): Promise<void> {
52
53
 
53
54
  if (!isProviderEnabled("codex")) return;
54
55
 
56
+ // See claude-hook.ts: the MCP server trusts this attribution, never the model.
57
+ recordAttributedCall({
58
+ provider: "codex",
59
+ sessionId: input.session_id,
60
+ toolName: input.tool_name,
61
+ args: input.tool_input,
62
+ });
63
+
55
64
  const blocked = blockedSessionCapability(
56
65
  input.session_id,
57
66
  input.tool_name,
@@ -17,6 +17,7 @@ import {
17
17
  resolveCursorMcpServer,
18
18
  type CursorMcpHookInput,
19
19
  } from "../cursor-mcp-policy";
20
+ import { recordAttributedCall } from "../mesh/call-scope";
20
21
  import { cursorRootSessionId } from "../sessions/cursor";
21
22
 
22
23
  async function readStdin(): Promise<string> {
@@ -47,6 +48,13 @@ async function main(): Promise<void> {
47
48
  }
48
49
  const rawSessionId = cursorConversationId(input);
49
50
  const sessionId = cursorRootSessionId(rawSessionId) ?? rawSessionId;
51
+ // See claude-hook.ts: Mesh events are published only for the calling session.
52
+ recordAttributedCall({
53
+ provider: "cursor",
54
+ sessionId,
55
+ toolName: input.tool_name,
56
+ args: input.tool_input,
57
+ });
50
58
  const server = resolveCursorMcpServer(input);
51
59
  const blocked = blockedSessionMcpServer(sessionId, server);
52
60
  if (blocked) {
@@ -13,10 +13,21 @@ Codex is started. The provider abstraction already admits Cursor and Grok Build
13
13
  without changing the core contract, while their unsupported phase-one start
14
14
  paths fail closed.
15
15
 
16
- The read-only MCP resource `granttap://mesh/current` exposes compact local state
17
- to an agent. Structured events use the existing `notify` tool so the public MCP
18
- tool allowlist remains exactly `connect`, `notify`, `ask_yes_no`, and `ask`.
19
- Setup and global provider configuration remain CLI-only.
16
+ `call-scope.ts` and `capability.ts` own caller identity. The MCP server cannot
17
+ see which agent called it, so each provider hook records a single-use
18
+ attribution for the exact call it observed inside its own session, and the
19
+ runtime derives the publishing execution from that instead of from anything the
20
+ model supplied. `capability.ts` mints the opaque per-execution token that
21
+ `scoped-view.ts` answers with, so a read returns one Project rather than
22
+ everything this computer knows. Structured events use the existing `notify`
23
+ tool so the public MCP tool allowlist remains exactly `connect`, `notify`,
24
+ `ask_yes_no`, and `ask`. Setup and global provider configuration remain
25
+ CLI-only.
26
+
27
+ `readiness.ts` decides whether a handoff may leave at all. A capsule transfers
28
+ committed facts, so uncommitted work blocks the handoff instead of silently
29
+ staying behind, and the destination refuses just as explicitly when it lacks the
30
+ named commit or when the capsule's resource claims overlap another execution.
20
31
 
21
32
  Every event is schema-bounded and sent under an independent task key; snapshots
22
33
  use an independent project key. Resource claims are advisory, expire without a
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Trusted attribution for GrantTap MCP calls.
3
+ *
4
+ * The MCP server sees a tool call but not which agent session made it, so a
5
+ * model that learns another execution's session id could otherwise publish
6
+ * events in its name. Provider hooks do know the session: they run inside it
7
+ * and receive the exact arguments. Each hook records a single-use, short-lived
8
+ * attribution keyed by those arguments, and the MCP server consumes it instead
9
+ * of trusting anything the model claimed.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { z } from "zod";
15
+ import { configDir } from "../config";
16
+
17
+ const CALL_TTL_MS = 120_000;
18
+ const MAX_RECORDS = 32;
19
+
20
+ const AttributedCall = z.object({
21
+ key: z.string().regex(/^[0-9a-f]{64}$/),
22
+ provider: z.enum(["claude", "codex", "cursor", "grok"]),
23
+ sessionId: z.string().trim().min(1).max(256),
24
+ expiresAt: z.number().positive(),
25
+ }).strict();
26
+ export type AttributedCall = z.infer<typeof AttributedCall>;
27
+
28
+ const CallFile = z.object({
29
+ version: z.literal(1),
30
+ calls: z.array(AttributedCall).max(MAX_RECORDS),
31
+ }).strict();
32
+
33
+ /** Normalize exactly like the wire schemas so hook and server agree byte for byte. */
34
+ function canonical(value: unknown): unknown {
35
+ if (typeof value === "string") return value.trim();
36
+ if (Array.isArray(value)) return value.map(canonical);
37
+ if (value && typeof value === "object") {
38
+ const out: Record<string, unknown> = {};
39
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
40
+ const item = (value as Record<string, unknown>)[key];
41
+ if (item !== undefined && item !== null) out[key] = canonical(item);
42
+ }
43
+ return out;
44
+ }
45
+ return value;
46
+ }
47
+
48
+ /** The GrantTap tool name as each provider spells it, reduced to one token. */
49
+ export function grantTapToolName(rawToolName: unknown): string | undefined {
50
+ const raw = String(rawToolName ?? "").trim();
51
+ const tool = raw.includes("__") ? raw.split("__").at(-1)! : raw;
52
+ return ["notify", "connect", "ask", "ask_yes_no"].includes(tool) ? tool : undefined;
53
+ }
54
+
55
+ export function meshCallKey(tool: string, args: unknown): string {
56
+ return createHash("sha256")
57
+ .update(JSON.stringify({ tool, args: canonical(args) }))
58
+ .digest("hex");
59
+ }
60
+
61
+ function callFilePath(): string {
62
+ return join(configDir(), "mesh-tool-calls.json");
63
+ }
64
+
65
+ function readCalls(now: number): AttributedCall[] {
66
+ try {
67
+ const parsed = CallFile.safeParse(JSON.parse(readFileSync(callFilePath(), "utf8")));
68
+ if (!parsed.success) return [];
69
+ return parsed.data.calls.filter((call) => call.expiresAt > now);
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ function writeCalls(calls: AttributedCall[]): void {
76
+ const path = callFilePath();
77
+ mkdirSync(configDir(), { recursive: true });
78
+ writeFileSync(
79
+ path,
80
+ `${JSON.stringify(CallFile.parse({ version: 1, calls: calls.slice(-MAX_RECORDS) }), null, 2)}\n`,
81
+ { mode: 0o600 },
82
+ );
83
+ chmodSync(path, 0o600);
84
+ }
85
+
86
+ /**
87
+ * Record that this provider session is about to make one GrantTap tool call.
88
+ * Hooks call this before returning their decision; it never blocks the hook.
89
+ */
90
+ export function recordAttributedCall(input: {
91
+ provider: AttributedCall["provider"];
92
+ sessionId: unknown;
93
+ toolName: unknown;
94
+ args: unknown;
95
+ now?: number;
96
+ }): void {
97
+ const tool = grantTapToolName(input.toolName);
98
+ const sessionId = String(input.sessionId ?? "").trim();
99
+ if (tool !== "notify" || !sessionId || sessionId.length > 256) return;
100
+ const now = input.now ?? Date.now();
101
+ try {
102
+ const call = AttributedCall.parse({
103
+ key: meshCallKey(tool, input.args),
104
+ provider: input.provider,
105
+ sessionId,
106
+ expiresAt: now + CALL_TTL_MS,
107
+ });
108
+ const kept = readCalls(now).filter((item) =>
109
+ item.key !== call.key || item.sessionId !== call.sessionId);
110
+ writeCalls([...kept, call]);
111
+ } catch {
112
+ // Attribution is best effort inside the hook; the MCP server fails closed.
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Resolve the session that actually made this call, then retire the record.
118
+ * Two live sessions claiming the same arguments are ambiguous and rejected.
119
+ */
120
+ export function consumeAttributedCall(
121
+ tool: string,
122
+ args: unknown,
123
+ now = Date.now(),
124
+ ): AttributedCall | undefined {
125
+ const key = meshCallKey(tool, args);
126
+ const live = readCalls(now);
127
+ const matches = live.filter((call) => call.key === key);
128
+ const sessions = new Set(matches.map((call) => call.sessionId));
129
+ if (matches.length === 0 || sessions.size !== 1) {
130
+ if (matches.length > 0) writeCalls(live.filter((call) => call.key !== key));
131
+ return undefined;
132
+ }
133
+ writeCalls(live.filter((call) => call.key !== key));
134
+ return matches[0];
135
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Opaque per-execution Mesh capabilities.
3
+ *
4
+ * A capability names exactly one live execution: its Project, its Task, its
5
+ * provider and its native session. It is minted only for a call the provider
6
+ * hook already attributed to that session, and it is returned only into that
7
+ * session's own tool result, so one agent can never obtain another's scope.
8
+ */
9
+ import { randomBytes } from "node:crypto";
10
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { z } from "zod";
13
+ import {
14
+ MeshEventType,
15
+ type ExecutionSessionLink,
16
+ type MeshSnapshot,
17
+ } from "../../../../packages/protocol/schema";
18
+ import { configDir } from "../config";
19
+ import { isProviderEnabled } from "../config/runtime";
20
+ import { localMeshStore } from "./local";
21
+
22
+ const CAPABILITY_TTL_MS = 12 * 60 * 60_000;
23
+ const MAX_CAPABILITIES = 64;
24
+
25
+ /** Ownership transfer is decided by the runtime after receipt checks, never by a tool call. */
26
+ const PUBLISHABLE_EVENT_TYPES = MeshEventType.options.filter((type) =>
27
+ type !== "HANDOFF_ACCEPTED" && type !== "HANDOFF_REJECTED");
28
+
29
+ const ExecutionCapability = z.object({
30
+ token: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
31
+ provider: z.enum(["claude", "codex", "cursor", "grok"]),
32
+ sessionId: z.string().trim().min(1).max(256),
33
+ projectId: z.string().trim().min(1).max(128),
34
+ taskId: z.string().trim().min(1).max(128),
35
+ allowedEventTypes: z.array(MeshEventType).min(1),
36
+ issuedAt: z.number().nonnegative(),
37
+ expiresAt: z.number().positive(),
38
+ }).strict();
39
+ export type ExecutionCapability = z.infer<typeof ExecutionCapability>;
40
+
41
+ const CapabilityFile = z.object({
42
+ version: z.literal(1),
43
+ capabilities: z.array(ExecutionCapability).max(MAX_CAPABILITIES),
44
+ }).strict();
45
+
46
+ export function meshCapabilityPath(): string {
47
+ return join(configDir(), "mesh-capabilities.json");
48
+ }
49
+
50
+ function readCapabilities(now: number): ExecutionCapability[] {
51
+ try {
52
+ const parsed = CapabilityFile.safeParse(JSON.parse(readFileSync(meshCapabilityPath(), "utf8")));
53
+ if (!parsed.success) return [];
54
+ return parsed.data.capabilities.filter((item) => item.expiresAt > now);
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ function writeCapabilities(capabilities: ExecutionCapability[]): void {
61
+ const path = meshCapabilityPath();
62
+ mkdirSync(configDir(), { recursive: true });
63
+ writeFileSync(
64
+ path,
65
+ `${JSON.stringify(
66
+ CapabilityFile.parse({ version: 1, capabilities: capabilities.slice(-MAX_CAPABILITIES) }),
67
+ null,
68
+ 2,
69
+ )}\n`,
70
+ { mode: 0o600 },
71
+ );
72
+ chmodSync(path, 0o600);
73
+ }
74
+
75
+ export type ExecutionScope = {
76
+ execution: ExecutionSessionLink;
77
+ snapshot: MeshSnapshot;
78
+ };
79
+
80
+ /** The one live, enabled coding-agent execution that owns this native session. */
81
+ export function liveExecutionScope(sessionId: string): ExecutionScope | undefined {
82
+ const store = localMeshStore();
83
+ for (const projectId of store.projectIds()) {
84
+ const snapshot = store.snapshot(projectId);
85
+ const execution = snapshot?.executions.find((item) =>
86
+ item.sessionId === sessionId && item.endedAt == null);
87
+ if (!snapshot || !execution) continue;
88
+ if (execution.provider === "grok_bot" || !isProviderEnabled(execution.provider)) return undefined;
89
+ return { execution, snapshot };
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ /** Mint or reuse the capability for one attributed session. */
95
+ export function executionCapabilityFor(
96
+ sessionId: string,
97
+ now = Date.now(),
98
+ ): ExecutionCapability | undefined {
99
+ const scope = liveExecutionScope(sessionId);
100
+ if (!scope) return undefined;
101
+ const live = readCapabilities(now);
102
+ const existing = live.find((item) =>
103
+ item.sessionId === sessionId
104
+ && item.taskId === scope.execution.taskId
105
+ && item.projectId === scope.snapshot.projectId);
106
+ if (existing) return existing;
107
+ const capability = ExecutionCapability.parse({
108
+ token: randomBytes(32).toString("base64url"),
109
+ provider: scope.execution.provider,
110
+ sessionId,
111
+ projectId: scope.snapshot.projectId,
112
+ taskId: scope.execution.taskId,
113
+ allowedEventTypes: PUBLISHABLE_EVENT_TYPES,
114
+ issuedAt: now,
115
+ expiresAt: now + CAPABILITY_TTL_MS,
116
+ });
117
+ writeCapabilities([...live.filter((item) => item.sessionId !== sessionId), capability]);
118
+ return capability;
119
+ }
120
+
121
+ /** Resolve a presented capability; an ended or disabled execution resolves to nothing. */
122
+ export function resolveExecutionCapability(
123
+ token: unknown,
124
+ now = Date.now(),
125
+ ): ExecutionCapability | undefined {
126
+ const value = String(token ?? "").trim();
127
+ if (!value) return undefined;
128
+ const capability = readCapabilities(now).find((item) => item.token === value);
129
+ if (!capability) return undefined;
130
+ const scope = liveExecutionScope(capability.sessionId);
131
+ if (!scope
132
+ || scope.snapshot.projectId !== capability.projectId
133
+ || scope.execution.taskId !== capability.taskId) return undefined;
134
+ return capability;
135
+ }
@@ -29,6 +29,22 @@ function git(cwd: string, args: string[]): string | undefined {
29
29
  }
30
30
  }
31
31
 
32
+ /**
33
+ * Whether this checkout has changes that a commit-based handoff would leave
34
+ * behind. `undefined` means the probe could not answer, which callers must not
35
+ * read as "clean".
36
+ */
37
+ export function hasUncommittedWork(cwd: string): boolean | undefined {
38
+ try {
39
+ execFileSync("git", ["-C", cwd, "diff", "--quiet", "HEAD"], {
40
+ stdio: "ignore", timeout: 2_000,
41
+ });
42
+ return false;
43
+ } catch (error) {
44
+ return (error as { status?: number }).status === 1 ? true : undefined;
45
+ }
46
+ }
47
+
32
48
  export function inspectRepository(cwd: string): RepositoryFacts {
33
49
  const cached = repositoryCache.get(cwd);
34
50
  if (cached) return cached;
@@ -98,6 +114,7 @@ export function linkSessionsToProjects(
98
114
  workspace: cwd,
99
115
  branch: session.branch,
100
116
  worktree: repository.worktree,
117
+ uncommitted: hasUncommittedWork(repository.worktree ?? cwd),
101
118
  startedAt: session.startedAt,
102
119
  });
103
120
  return { ...session, projectId, taskId, computerId, worktree: repository.worktree };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Handoff readiness.
3
+ *
4
+ * A Task Capsule carries facts, not files: the destination starts a worktree
5
+ * from a commit. Anything still uncommitted stays on the source computer even
6
+ * though the Task visibly continues elsewhere, so a handoff that would lose it
7
+ * is refused instead of silently narrowing to committed state.
8
+ */
9
+ import type { ResourceClaim, TaskCapsule } from "../../../../packages/protocol/schema";
10
+
11
+ export const UNCOMMITTED_WORK_REASON =
12
+ "This task has uncommitted changes. Commit or checkpoint them before moving the task.";
13
+
14
+ export type HandoffCheckId = "repository" | "workingTree" | "claims" | "targetAgent";
15
+
16
+ export type HandoffCheck = {
17
+ id: HandoffCheckId;
18
+ ready: boolean;
19
+ detail: string;
20
+ };
21
+
22
+ export type HandoffReadiness = {
23
+ ready: boolean;
24
+ blockedReason?: string;
25
+ checks: HandoffCheck[];
26
+ };
27
+
28
+ export type HandoffReadinessInput = {
29
+ capsule?: TaskCapsule;
30
+ targetProviderEnabled: boolean;
31
+ conflicts: ResourceClaim[];
32
+ };
33
+
34
+ function check(id: HandoffCheckId, ready: boolean, detail: string): HandoffCheck {
35
+ return { id, ready, detail };
36
+ }
37
+
38
+ /** Every blocking condition the source computer can answer before publishing. */
39
+ export function handoffReadiness(input: HandoffReadinessInput): HandoffReadiness {
40
+ const { capsule } = input;
41
+ const conflict = input.conflicts.at(0);
42
+ const checks: HandoffCheck[] = [
43
+ check(
44
+ "repository",
45
+ Boolean(capsule),
46
+ capsule
47
+ ? `Commit ${capsule.latestCommit ?? capsule.baseSha} in ${capsule.repository}`
48
+ : "No git repository, task, or commit was found for this execution.",
49
+ ),
50
+ check(
51
+ "workingTree",
52
+ Boolean(capsule) && capsule?.dirtyDiffHash == null,
53
+ capsule?.dirtyDiffHash == null ? "Clean" : UNCOMMITTED_WORK_REASON,
54
+ ),
55
+ check(
56
+ "claims",
57
+ !conflict,
58
+ conflict
59
+ ? `${conflict.ownerSessionId} currently claims ${conflict.resource}.`
60
+ : "No overlapping resource claims",
61
+ ),
62
+ check(
63
+ "targetAgent",
64
+ input.targetProviderEnabled,
65
+ input.targetProviderEnabled
66
+ ? "Ready"
67
+ : "The target agent is disabled in GrantTap Settings.",
68
+ ),
69
+ ];
70
+ const blocked = checks.find((item) => !item.ready);
71
+ return { ready: !blocked, blockedReason: blocked?.detail, checks };
72
+ }
@@ -0,0 +1,37 @@
1
+ /** The seams the Mesh runtime needs from this computer: state, sessions, git, and transport. */
2
+ import type { RelayClient } from "../../../../packages/core/relay-client";
3
+ import type {
4
+ MeshEvent,
5
+ MeshProvider,
6
+ MeshSnapshot,
7
+ SessionInfo,
8
+ } from "../../../../packages/protocol/schema";
9
+ import type { createCodexSession, deliverToSession } from "../reply";
10
+ import type { MeshStore } from "./store";
11
+ import type { createHandoffWorktree } from "./worktree";
12
+
13
+ type StartResult = Awaited<ReturnType<typeof createCodexSession>>;
14
+ type WorktreeResult = NonNullable<ReturnType<typeof createHandoffWorktree>>;
15
+
16
+ export type MeshRuntimeDependencies = {
17
+ store: () => MeshStore;
18
+ sessions: () => SessionInfo[];
19
+ computer: () => string;
20
+ now: () => number;
21
+ eventId: () => string;
22
+ providerEnabled: (provider: Exclude<MeshProvider, "grok_bot">) => boolean;
23
+ start: (provider: MeshProvider, prompt: string, cwd: string) => Promise<StartResult>;
24
+ deliver: typeof deliverToSession;
25
+ send: (
26
+ client: RelayClient,
27
+ payload: MeshEvent | MeshSnapshot,
28
+ options: { ttlMs: number; wake?: boolean },
29
+ ) => Promise<void>;
30
+ worktree: (
31
+ repository: string,
32
+ taskId: string,
33
+ provider: MeshProvider,
34
+ revision: string,
35
+ ) => WorktreeResult | undefined | null;
36
+ hasCommit: (repository: string, revision: string) => boolean;
37
+ };