granttap-mcp 0.8.75 → 0.8.78

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 (173) hide show
  1. package/LICENSE +72 -202
  2. package/LICENSE-APACHE-2.0.txt +202 -0
  3. package/NOTICE +4 -2
  4. package/README.md +13 -6
  5. package/THIRD_PARTY_NOTICES.md +3 -3
  6. package/apps/bridge/src/account-recovery/README.md +1 -1
  7. package/apps/bridge/src/approvals/cancellation/README.md +8 -0
  8. package/apps/bridge/src/approvals/cancellation/index.ts +20 -0
  9. package/apps/bridge/src/approvals/index.ts +6 -0
  10. package/apps/bridge/src/bin/cli/setup.ts +5 -8
  11. package/apps/bridge/src/bin/hooks/cursor-hook.ts +6 -1
  12. package/apps/bridge/src/bin/runtime/cursor-mcp-hook.ts +6 -1
  13. package/apps/bridge/src/bin/runtime/monitor.ts +16 -7
  14. package/apps/bridge/src/capabilities/README.md +1 -1
  15. package/apps/bridge/src/claude-questions/README.md +1 -1
  16. package/apps/bridge/src/cloud-runners/README.md +1 -1
  17. package/apps/bridge/src/codex-desktop/README.md +31 -0
  18. package/apps/bridge/src/codex-desktop/connection.ts +104 -0
  19. package/apps/bridge/src/codex-desktop/controls.ts +21 -0
  20. package/apps/bridge/src/codex-desktop/index.ts +78 -0
  21. package/apps/bridge/src/codex-desktop/receipts.ts +55 -0
  22. package/apps/bridge/src/codex-desktop/socket-path.ts +25 -0
  23. package/apps/bridge/src/codex-hook-trust/README.md +1 -1
  24. package/apps/bridge/src/config/README.md +1 -1
  25. package/apps/bridge/src/config/runtime/index.ts +3 -2
  26. package/apps/bridge/src/configuration-delivery/README.md +1 -1
  27. package/apps/bridge/src/delivery/index.ts +14 -0
  28. package/apps/bridge/src/device-network/README.md +2 -2
  29. package/apps/bridge/src/engine/README.md +17 -4
  30. package/apps/bridge/src/engine/distribution/README.md +20 -0
  31. package/apps/bridge/src/engine/distribution/artifacts.ts +35 -0
  32. package/apps/bridge/src/engine/distribution/download.ts +29 -0
  33. package/apps/bridge/src/engine/distribution/index.ts +3 -0
  34. package/apps/bridge/src/engine/distribution/installation.ts +44 -0
  35. package/apps/bridge/src/engine/distribution/monitor/README.md +8 -0
  36. package/apps/bridge/src/engine/distribution/monitor/index.ts +47 -0
  37. package/apps/bridge/src/engine/distribution/storage.ts +31 -0
  38. package/apps/bridge/src/engine/distribution/windows-storage.ts +32 -0
  39. package/apps/bridge/src/engine/protocol/engine-declaration.ts +5 -1
  40. package/apps/bridge/src/engine/runtime/engine-client.ts +2 -1
  41. package/apps/bridge/src/engine/runtime/engine-supervisor.ts +3 -1
  42. package/apps/bridge/src/engine/startup/README.md +15 -0
  43. package/apps/bridge/src/engine/startup/activation-entry.ts +3 -0
  44. package/apps/bridge/src/engine/startup/activation.ts +18 -0
  45. package/apps/bridge/src/engine/startup/index.ts +3 -0
  46. package/apps/bridge/src/engine/startup/local-endpoint.ts +13 -0
  47. package/apps/bridge/src/engine/startup/recovery.ts +45 -0
  48. package/apps/bridge/src/host/approval-presentation/README.md +1 -1
  49. package/apps/bridge/src/host/session-key-store/README.md +1 -1
  50. package/apps/bridge/src/install/inspect.ts +2 -0
  51. package/apps/bridge/src/install/linux-service/README.md +19 -0
  52. package/apps/bridge/src/install/linux-service/definition.ts +34 -0
  53. package/apps/bridge/src/install/linux-service/index.ts +107 -0
  54. package/apps/bridge/src/install/monitor-helper.ts +13 -35
  55. package/apps/bridge/src/install/monitor-launchd/README.md +8 -0
  56. package/apps/bridge/src/install/monitor-launchd/index.ts +25 -0
  57. package/apps/bridge/src/machine-load/README.md +1 -1
  58. package/apps/bridge/src/machine-load/command-process/README.md +1 -1
  59. package/apps/bridge/src/managed-admission/README.md +1 -1
  60. package/apps/bridge/src/mesh/README.md +1 -1
  61. package/apps/bridge/src/mesh/catalog/README.md +1 -1
  62. package/apps/bridge/src/mesh/context/README.md +1 -1
  63. package/apps/bridge/src/mesh/knowledge/README.md +1 -1
  64. package/apps/bridge/src/mesh/repository-details/README.md +1 -1
  65. package/apps/bridge/src/mesh/restrictions/README.md +1 -1
  66. package/apps/bridge/src/mesh/runtime/graph/README.md +1 -1
  67. package/apps/bridge/src/monitor/README.md +1 -1
  68. package/apps/bridge/src/monitor/handlers/catalog.ts +7 -4
  69. package/apps/bridge/src/monitor/handlers/inbound.ts +6 -1
  70. package/apps/bridge/src/monitor/handlers/receipts.ts +4 -2
  71. package/apps/bridge/src/monitor/handlers/user-message.ts +19 -5
  72. package/apps/bridge/src/monitor/mesh-publication/README.md +1 -1
  73. package/apps/bridge/src/monitor/message-delivery/README.md +5 -1
  74. package/apps/bridge/src/monitor/message-delivery/index.ts +6 -4
  75. package/apps/bridge/src/monitor/support/runtime.ts +5 -1
  76. package/apps/bridge/src/policy/README.md +1 -1
  77. package/apps/bridge/src/policy/shell/README.md +1 -1
  78. package/apps/bridge/src/provider-account-status/README.md +1 -1
  79. package/apps/bridge/src/provider-model-catalog/README.md +1 -1
  80. package/apps/bridge/src/reply/README.md +12 -1
  81. package/apps/bridge/src/reply/cursor-context/README.md +18 -0
  82. package/apps/bridge/src/reply/cursor-context/index.ts +64 -0
  83. package/apps/bridge/src/reply/cursor-conversation/README.md +21 -0
  84. package/apps/bridge/src/reply/cursor-conversation/actions.ts +44 -0
  85. package/apps/bridge/src/reply/cursor-conversation/call-context/README.md +9 -0
  86. package/apps/bridge/src/reply/cursor-conversation/call-context/index.ts +32 -0
  87. package/apps/bridge/src/reply/cursor-conversation/index.ts +86 -0
  88. package/apps/bridge/src/reply/cursor-conversation/interactions.ts +39 -0
  89. package/apps/bridge/src/reply/cursor-conversation/permissions.ts +67 -0
  90. package/apps/bridge/src/reply/cursor-conversation/questions.ts +66 -0
  91. package/apps/bridge/src/reply/cursor-conversation/transport.ts +136 -0
  92. package/apps/bridge/src/reply/index.ts +3 -0
  93. package/apps/bridge/src/reply/payload/attachments.ts +29 -5
  94. package/apps/bridge/src/reply/payload/identity.ts +13 -0
  95. package/apps/bridge/src/reply/payload/types.ts +6 -2
  96. package/apps/bridge/src/reply/process/codex-headless.ts +8 -2
  97. package/apps/bridge/src/reply/process/index.ts +8 -4
  98. package/apps/bridge/src/reply/process/provider-headless.ts +6 -36
  99. package/apps/bridge/src/session-catalog/README.md +1 -1
  100. package/apps/bridge/src/session-images/README.md +1 -1
  101. package/apps/bridge/src/sessions/README.md +1 -1
  102. package/apps/bridge/src/sessions/cursor/README.md +1 -1
  103. package/apps/bridge/src/sessions/cursor/activity.ts +4 -0
  104. package/apps/bridge/src/sessions/cursor/attention/README.md +1 -1
  105. package/apps/bridge/src/sessions/cursor/catalog.ts +3 -0
  106. package/apps/bridge/src/sessions/cursor/conversations/README.md +13 -0
  107. package/apps/bridge/src/sessions/cursor/conversations/history.ts +33 -0
  108. package/apps/bridge/src/sessions/cursor/conversations/index.ts +80 -0
  109. package/apps/bridge/src/sessions/cursor/conversations/storage.ts +68 -0
  110. package/apps/bridge/src/sessions/cursor/executions/README.md +12 -0
  111. package/apps/bridge/src/sessions/cursor/executions/index.ts +46 -0
  112. package/apps/bridge/src/sessions/cursor/index.ts +2 -0
  113. package/apps/bridge/src/sessions/cursor/scan.ts +2 -1
  114. package/apps/bridge/src/sessions/scan/codex/README.md +1 -1
  115. package/apps/bridge/src/sessions/telemetry/README.md +1 -1
  116. package/apps/bridge/src/task-automation/README.md +1 -1
  117. package/apps/bridge/src/task-control/README.md +1 -1
  118. package/apps/bridge/src/task-control/index.ts +4 -0
  119. package/apps/bridge/src/task-dispatch/README.md +1 -1
  120. package/apps/bridge/src/task-files/README.md +1 -1
  121. package/apps/bridge/src/task-network/README.md +1 -1
  122. package/apps/bridge/src/task-network/configuration/README.md +7 -1
  123. package/apps/bridge/src/task-network/configuration/runtime.ts +1 -1
  124. package/apps/bridge/src/task-network/configuration/sync.ts +21 -4
  125. package/apps/bridge/src/task-origins/README.md +1 -1
  126. package/apps/bridge/src/task-routing/README.md +1 -1
  127. package/apps/bridge/src/task-routing/resources/README.md +1 -1
  128. package/apps/bridge/src/task-transfer/README.md +1 -1
  129. package/apps/bridge/src/task-transfer/continuation/README.md +1 -1
  130. package/apps/bridge/src/task-transfer/runtime/README.md +1 -1
  131. package/apps/bridge/src/transcript-history/README.md +1 -1
  132. package/apps/bridge/src/transcript-history/index.ts +8 -0
  133. package/apps/mcp/src/connection-center/README.md +1 -1
  134. package/apps/mcp/src/desktop/README.md +1 -1
  135. package/apps/mcp/src/desktop/delivery/README.md +1 -1
  136. package/apps/mcp/src/desktop/delivery/task-send.ts +1 -1
  137. package/apps/mcp/src/desktop/live-catalog/README.md +1 -1
  138. package/apps/mcp/src/desktop/models/README.md +1 -1
  139. package/apps/mcp/src/desktop/native-access/README.md +1 -1
  140. package/apps/mcp/src/desktop/network/README.md +1 -1
  141. package/apps/mcp/src/desktop/pairing/README.md +1 -1
  142. package/apps/mcp/src/desktop/policy/README.md +1 -1
  143. package/apps/mcp/src/desktop/project-transport/README.md +1 -1
  144. package/apps/mcp/src/desktop/provider-hooks/README.md +1 -1
  145. package/apps/mcp/src/desktop/provider-storage/README.md +2 -2
  146. package/apps/mcp/src/desktop/task-control/README.md +1 -1
  147. package/apps/mcp/src/desktop/task-queue/README.md +1 -1
  148. package/apps/mcp/src/desktop/task-transfer/README.md +1 -1
  149. package/apps/mcp/src/desktop/workspace/README.md +1 -1
  150. package/apps/mcp/src/http-service/README.md +1 -1
  151. package/apps/mcp/src/http-service/common.ts +2 -0
  152. package/apps/mcp/src/http-service/installer.ts +8 -1
  153. package/apps/mcp/src/http-service/snapshot.ts +9 -0
  154. package/apps/mcp/src/mcp-tools/README.md +1 -1
  155. package/apps/mcp/src/oauth/README.md +1 -1
  156. package/bin/granttap-mcp.mjs +3 -1
  157. package/cursor-plugin/.cursor-plugin/plugin.json +2 -2
  158. package/cursor-plugin/LICENSE +72 -202
  159. package/cursor-plugin/NOTICE +4 -2
  160. package/cursor-plugin/README.md +3 -3
  161. package/cursor-plugin/mcp.json +1 -1
  162. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  163. package/package.json +4 -3
  164. package/packages/core/README.md +1 -1
  165. package/packages/core/ciphertext-recipient/README.md +1 -1
  166. package/packages/core/outbound-scheduler/README.md +1 -1
  167. package/packages/core/socket-lifecycle/README.md +1 -1
  168. package/packages/core/store-file-lock/README.md +1 -1
  169. package/packages/core/task-job-http/README.md +1 -1
  170. package/packages/protocol/messages/README.md +1 -1
  171. package/packages/protocol/messages/provider-hooks/README.md +1 -1
  172. package/packages/protocol/task-dispatch/README.md +1 -1
  173. package/packages/protocol/task-transfer/README.md +1 -1
@@ -11,7 +11,7 @@ import {
11
11
  type InstallResult,
12
12
  } from "../../install";
13
13
  import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from "../../pairing";
14
- import { declareEngine } from "../../engine/protocol/engine-declaration";
14
+ import { prepareEngine } from "../../engine/distribution";
15
15
  import { isMachineConfigured } from "../../../../mcp/src/status/pairing-status";
16
16
  import { removeCursorUserGrantTap } from "../../../../mcp/src/cursor-config";
17
17
  import { installHttpMcpService, waitForHttpMcpHealth } from "../../../../mcp/src/http-service";
@@ -59,15 +59,14 @@ async function main(): Promise<void> {
59
59
  // Declared before the helper is written, so the LaunchAgent picks the engine
60
60
  // up in the same run rather than needing a second one.
61
61
  const engineFlag = process.argv.indexOf("--engine");
62
- const engine = declareEngine(
63
- engineFlag >= 0 ? process.argv[engineFlag + 1] : undefined,
64
- );
62
+ if (engineFlag >= 0 && !process.argv[engineFlag + 1]) throw new Error("Choose an executable after --engine");
63
+ const engine = await prepareEngine(engineFlag >= 0 ? process.argv[engineFlag + 1] : undefined);
65
64
  const helper = installMonitorHelper();
66
65
  let httpService: InstallResult | null = null;
67
66
  let cursorConfig: InstallResult | null = null;
68
67
  const mcpClientInstalled = before.agents.some((item) => item.installed);
69
68
  if (before.cursor.installed || mcpClientInstalled) {
70
- httpService = installHttpMcpService();
69
+ httpService = installHttpMcpService({ forceReload: true });
71
70
  if (process.platform === "win32" && httpService.status !== "manual"
72
71
  && !await waitForHttpMcpHealth(undefined, 15_000)) {
73
72
  httpService = { status: "manual", detail: "Windows background service did not become healthy." };
@@ -112,9 +111,7 @@ async function main(): Promise<void> {
112
111
  ? "Next: keep the GrantTap plugin. Pair and change settings in the GrantTap app, not Cursor MCP settings."
113
112
  : !paired
114
113
  ? "Next: pair from the GrantTap plugin connection card or the GrantTap app."
115
- : engine
116
- ? "Governance is live. Restart the helper to pick it up: launchctl kickstart -k gui/$(id -u)/com.granttap.monitor"
117
- : "Governance stays off until an engine is found. Point at one with: granttap setup --engine <path>",
114
+ : "Ready. Continue from GrantTap on your controller device.",
118
115
  "",
119
116
  ].join("\n"));
120
117
  }
@@ -112,6 +112,11 @@ async function continueApproval(
112
112
  return;
113
113
  }
114
114
 
115
+ if (process.env.GRANTTAP_CURSOR_ACP === "1") {
116
+ nativeAsk("GrantTap will show this action through the active Cursor conversation.");
117
+ return;
118
+ }
119
+
115
120
  const timeoutMs = Number(process.env.GRANTTAP_APPROVAL_TIMEOUT_MS ?? 60_000);
116
121
  let decision;
117
122
  try {
@@ -137,6 +142,6 @@ function cursorCwd(input: CursorHookInput): string | undefined {
137
142
  }
138
143
 
139
144
  main().catch(() => {
140
- nativeAsk("GrantTap shell hook failed; use Cursor approval.");
145
+ deny("GrantTap could not verify this Cursor Task's execution scope.");
141
146
  process.exit(0);
142
147
  });
@@ -122,6 +122,11 @@ async function continueApproval(
122
122
  return;
123
123
  }
124
124
 
125
+ if (process.env.GRANTTAP_CURSOR_ACP === "1") {
126
+ nativeAsk("GrantTap will show this action through the active Cursor conversation.");
127
+ return;
128
+ }
129
+
125
130
  const timeoutMs = Number(process.env.GRANTTAP_APPROVAL_TIMEOUT_MS ?? 60_000);
126
131
  let decision;
127
132
  try {
@@ -170,6 +175,6 @@ function cursorWorkspaceRoot(input: CursorMcpHookInput): string | undefined {
170
175
  }
171
176
 
172
177
  main().catch(() => {
173
- nativeAsk("GrantTap MCP hook failed; use Cursor approval.");
178
+ deny("GrantTap could not verify this Cursor Task's execution scope.");
174
179
  process.exit(0);
175
180
  });
@@ -3,8 +3,10 @@ import { join } from "node:path";
3
3
  import { RelayClient } from "../../../../../packages/core/relay-client";
4
4
  import { configDir, loadConfig, machineConfigPath } from "../../config";
5
5
  import { EngineSupervisor } from "../../engine/runtime/engine-supervisor";
6
+ import { prepareMonitorEngine } from "../../engine/distribution";
7
+ import { startEngineRecovery } from "../../engine/startup";
6
8
  import { meshSnapshotsAsync } from "../../mesh/runtime";
7
- import { publishedCatalog, recoverCatalogBindings } from "../../session-catalog";
9
+ import { publishedCatalog } from "../../session-catalog";
8
10
  import { recoverLocalGraphBindings } from "../../mesh/runtime/graph/binding-sync";
9
11
  import { startSessionMonitor } from "../../monitor";
10
12
  import { controllerPeerGate } from "../../pairing/controllers";
@@ -22,6 +24,7 @@ let stopping = false;
22
24
  let stopEndpointPublisher = () => {};
23
25
  let stopTaskDispatch = () => {};
24
26
  let stopTaskNetwork = () => {};
27
+ let stopEngineRecovery = () => {};
25
28
 
26
29
  try {
27
30
  const cfg = applyNetworkRoute(loadConfig(machineConfigPath()));
@@ -47,6 +50,7 @@ try {
47
50
  const stop = (): void => {
48
51
  if (stopping) return;
49
52
  stopping = true;
53
+ stopEngineRecovery();
50
54
  stopTaskDispatch();
51
55
  stopTaskNetwork();
52
56
  monitor.close();
@@ -58,8 +62,9 @@ const stop = (): void => {
58
62
  process.on("SIGINT", stop);
59
63
  process.on("SIGTERM", stop);
60
64
 
61
- void recoverCatalogBindings({
62
- engineAvailable: async () => {
65
+ stopEngineRecovery = startEngineRecovery({
66
+ prepare: prepareMonitorEngine,
67
+ available: async () => {
63
68
  const health = await engine.ensureAvailable();
64
69
  if (health.state !== "disabled" && health.state !== "healthy") {
65
70
  process.stderr.write(`[granttap-mcp] Project engine ${health.state}; legacy behavior remains active\n`);
@@ -74,9 +79,13 @@ void recoverCatalogBindings({
74
79
  throw error;
75
80
  }
76
81
  },
77
- snapshots: () => {
82
+ recover: async () => {
78
83
  const known = publishedCatalog(client);
79
- return meshSnapshotsAsync(known ? [...known.sessions, ...(known.history ?? [])] : []);
84
+ const snapshots = await meshSnapshotsAsync(known ? [...known.sessions, ...(known.history ?? [])] : []);
85
+ await recoverLocalGraphBindings(snapshots);
86
+ await monitor.publish();
80
87
  },
81
- recover: recoverLocalGraphBindings,
82
- }).then(() => monitor.publish()).catch(() => undefined);
88
+ failed: error => {
89
+ process.stderr.write(`[granttap-mcp] Engine recovery will retry (${error instanceof Error ? error.name : "unknown"})\n`);
90
+ },
91
+ });
@@ -11,4 +11,4 @@ Catalog observations and Project requests do not install a skill or native MCP
11
11
  configuration on another host. Availability requires a separate native apply
12
12
  and independently observed initialization; that rollout is still unfinished.
13
13
 
14
- License: this module is distributed under the Apache License 2.0 in the repository-root `LICENSE` file.
14
+ License: this module is distributed under the GrantTap Commercial Source License in the repository-root `LICENSE` file.
@@ -10,4 +10,4 @@ an `answers` mapping using Claude's documented PreToolUse `updatedInput` contrac
10
10
 
11
11
  Tests: `tests/`. Engineering contract: the Apple repository `AGENTS.md`.
12
12
 
13
- License: Apache License 2.0; see the repository LICENSE.
13
+ License: GrantTap Commercial Source License; see the repository LICENSE.
@@ -45,4 +45,4 @@ The first message is the Task Capsule: goal, status, repository, branch and
45
45
  commit, the person's comment, decisions and remaining work, bounded to 12,000
46
46
  characters. The visible conversation archive is not uploaded.
47
47
 
48
- Tests: [`tests/`](tests/). Apache License 2.0, see [LICENSE](../../../../LICENSE).
48
+ Tests: [`tests/`](tests/). GrantTap Commercial Source License, see [LICENSE](../../../../LICENSE).
@@ -0,0 +1,31 @@
1
+ # Codex desktop delivery
2
+
3
+ Public entry: `deliverToCodexDesktop` in `index.ts`. A paired controller's follow-up
4
+ goes to the native owner of the existing conversation, including between turns.
5
+ Starting a second `codex exec resume` against that owner's thread would conflict
6
+ with its single writer. New Tasks continue to use the provider's CLI creation path.
7
+
8
+ The local desktop coordination protocol is versioned and differs from the public
9
+ app-server WebSocket transport. This adapter implements its framing independently;
10
+ no desktop application source is bundled. The existing socket must be private,
11
+ owned by the current user, and inside the configured Codex home. Nothing creates,
12
+ replaces, or restarts the native router. Windows has no tested desktop socket path
13
+ and keeps the CLI path.
14
+
15
+ The native owner inherits its workspace and approval settings. Explicit per-Task
16
+ sandbox settings and model/effort are carried with the turn. Disabled MCP settings
17
+ that cannot be enforced through the owner are refused. Protocol errors or an
18
+ unconfirmed submission never fall back to a second writer. Only a missing socket
19
+ or an explicit owner-not-found response permits headless resume.
20
+
21
+ The original message ID is carried into Codex. A private, cross-process receipt
22
+ records a hash of the intent before dispatch and native turn ID after acceptance.
23
+ An uncertain or crashed dispatch is not replayed. A native receipt produces no
24
+ synthetic assistant reply or completed-run digest; the existing transcript carries
25
+ the actual work. Pause sends the user's stop request to the same owner. Document
26
+ attachments remain in private staging for up to a day after admission; the monitor
27
+ removes expired stages. Image-only stages are removed once native admission has
28
+ consumed them. Tests exercise native delivery, framing, socket trust, durable
29
+ deduplication, and refusal without launching the CLI.
30
+
31
+ License: GrantTap Commercial Source License in the repository-root `LICENSE`.
@@ -0,0 +1,104 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { connect, type Socket } from 'node:net';
3
+
4
+ type Response = { type: 'response'; requestId: string; method?: string; resultType: string;
5
+ handledByClientId?: string; result?: any; error?: string };
6
+ type Pending = { method: string; target?: string; resolve: (response: Response) => void;
7
+ reject: (error: Error) => void; timer: NodeJS.Timeout };
8
+ const MAX_FRAME_BYTES = 8 * 1024 * 1024;
9
+
10
+ /** The desktop coordination socket is length-prefixed JSON, not another app-server. */
11
+ export class DesktopConnection {
12
+ private readonly socket: Socket;
13
+ private readonly pending = new Map<string, Pending>();
14
+ private buffered = Buffer.alloc(0);
15
+ private clientId = 'initializing-client';
16
+ private closed = false;
17
+ private readonly deadline: number;
18
+
19
+ constructor(path: string, timeoutMs: number) {
20
+ this.deadline = Date.now() + Math.min(5_000, timeoutMs);
21
+ this.socket = connect(path);
22
+ this.socket.on('error', () => this.close());
23
+ this.socket.on('close', () => this.close());
24
+ this.socket.on('data', chunk => {
25
+ try { this.receive(chunk); } catch { this.close(); }
26
+ });
27
+ }
28
+
29
+ async initialize(): Promise<void> {
30
+ const response = await this.call('initialize', { clientType: 'desktop' }, 0);
31
+ const id = response.result?.clientId;
32
+ if (response.resultType !== 'success' || typeof id !== 'string' || !id || id.length > 180) {
33
+ throw new Error('Codex desktop initialization is unavailable.');
34
+ }
35
+ this.clientId = id;
36
+ }
37
+
38
+ call(method: string, params: unknown, version: number, target?: string): Promise<Response> {
39
+ return new Promise((resolve, reject) => {
40
+ if (this.closed) return reject(new Error('Codex desktop disconnected.'));
41
+ const requestId = randomUUID();
42
+ const timer = setTimeout(() => {
43
+ this.pending.delete(requestId);
44
+ reject(new Error('Codex desktop has not confirmed the request.'));
45
+ }, Math.max(1, this.deadline - Date.now()));
46
+ this.pending.set(requestId, { method, target, resolve, reject, timer });
47
+ this.send({ type: 'request', requestId, sourceClientId: this.clientId,
48
+ method, params, version, ...(target ? { targetClientId: target } : {}) });
49
+ });
50
+ }
51
+
52
+ close(): void {
53
+ if (this.closed) return;
54
+ this.closed = true;
55
+ this.socket.destroy();
56
+ this.buffered = Buffer.alloc(0);
57
+ for (const request of this.pending.values()) {
58
+ clearTimeout(request.timer);
59
+ request.reject(new Error('Codex desktop disconnected.'));
60
+ }
61
+ this.pending.clear();
62
+ }
63
+
64
+ private send(value: unknown): void {
65
+ const body = Buffer.from(JSON.stringify(value));
66
+ if (body.length > MAX_FRAME_BYTES) return this.close();
67
+ const prefix = Buffer.alloc(4);
68
+ prefix.writeUInt32LE(body.length);
69
+ this.socket.write(Buffer.concat([prefix, body]));
70
+ }
71
+
72
+ private receive(chunk: Buffer): void {
73
+ this.buffered = Buffer.concat([this.buffered, chunk]);
74
+ while (this.buffered.length >= 4) {
75
+ const size = this.buffered.readUInt32LE();
76
+ if (size === 0 || size > MAX_FRAME_BYTES) throw new Error('Invalid desktop frame.');
77
+ if (this.buffered.length < size + 4) return;
78
+ const message = JSON.parse(this.buffered.subarray(4, size + 4).toString());
79
+ this.buffered = this.buffered.subarray(size + 4);
80
+ this.handle(message);
81
+ }
82
+ }
83
+
84
+ private handle(message: any): void {
85
+ if (!message || typeof message !== 'object' || Array.isArray(message)) return;
86
+ if (message.type === 'client-discovery-request') {
87
+ this.send({ type: 'client-discovery-response', requestId: message.requestId,
88
+ response: { canHandle: false } });
89
+ } else if (message.type === 'request') {
90
+ this.send({ type: 'response', requestId: message.requestId,
91
+ resultType: 'error', error: 'no-handler-for-request' });
92
+ } else if (message.type === 'response') {
93
+ const request = this.pending.get(message.requestId);
94
+ if (!request) return;
95
+ if (message.method !== undefined && message.method !== request.method
96
+ || message.resultType === 'success' && request.target && message.handledByClientId !== request.target) {
97
+ return this.close();
98
+ }
99
+ this.pending.delete(message.requestId);
100
+ clearTimeout(request.timer);
101
+ request.resolve(message);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,21 @@
1
+ import { DesktopConnection } from './connection';
2
+ import { desktopSocketPath } from './socket-path';
3
+
4
+ /** A deliberate user stop reaches the owning desktop; no provider process is killed. */
5
+ export async function interruptCodexDesktop(sessionId: string): Promise<{ ok: boolean } | undefined> {
6
+ let connection: DesktopConnection | undefined;
7
+ try {
8
+ const path = desktopSocketPath();
9
+ if (!path) return undefined;
10
+ connection = new DesktopConnection(path, 2_500);
11
+ await connection.initialize();
12
+ const owner = await connection.call('thread-owner-discovery', { hostId: 'local', conversationId: sessionId }, 1);
13
+ if (owner.resultType === 'error' && owner.error === 'no-client-found') return undefined;
14
+ if (owner.resultType !== 'success' || !owner.handledByClientId) return { ok: false };
15
+ // Version 3 is the native explicit-user-stop form, without a stale expected turn ID.
16
+ const response = await connection.call('thread-follower-interrupt-turn',
17
+ { conversationId: sessionId, mode: 'user-stop', expectedTurnId: null }, 3, owner.handledByClientId);
18
+ return { ok: response.resultType === 'success' && response.result?.ok === true };
19
+ } catch { return { ok: false }; }
20
+ finally { connection?.close(); }
21
+ }
@@ -0,0 +1,78 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import type { SessionInfo } from '../../../../packages/protocol/schema';
4
+ import { loadRuntimeConfig } from '../config';
5
+ import type { DeliveryOptions, ReplyResult } from '../reply';
6
+ import { DesktopConnection } from './connection';
7
+ import { DesktopReceipt } from './receipts';
8
+ import { desktopSocketPath } from './socket-path';
9
+ export { interruptCodexDesktop } from './controls';
10
+
11
+ const UNAVAILABLE = 'Codex desktop is not ready to receive this message. Keep the original chat open and try again.';
12
+ const UNCERTAIN = 'Codex has not confirmed this message. Check the original chat before sending it again.';
13
+
14
+ /** Undefined means no desktop owner exists; only then may the caller resume a CLI. */
15
+ export async function deliverToCodexDesktop(input: {
16
+ session: SessionInfo; text: string; images: string[]; options: DeliveryOptions; timeoutMs: number;
17
+ }): Promise<ReplyResult | undefined> {
18
+ let connection: DesktopConnection | undefined;
19
+ let dispatched = false;
20
+ const { session, text, images, options } = input;
21
+ const accepted: ReplyResult = { ok: true, text: '', sessionId: session.sessionId, delivery: 'native' };
22
+ try {
23
+ const policy = loadRuntimeConfig();
24
+ const request = turnRequest(input, policy.sessionAccess[session.sessionId]);
25
+ const intent = options.intentDigest ?? { text,
26
+ images: images.map(path => createHash('sha256').update(readFileSync(path)).digest('hex')), options };
27
+ const receipt = new DesktopReceipt(session.sessionId, request.clientUserMessageId, intent);
28
+ const prior = receipt.lookup();
29
+ if (prior === 'accepted') return accepted;
30
+ if (prior) return refusal(prior);
31
+ const path = desktopSocketPath();
32
+ if (!path) return undefined;
33
+ connection = new DesktopConnection(path, input.timeoutMs);
34
+ await connection.initialize();
35
+ const owner = await connection.call('thread-owner-discovery',
36
+ { hostId: 'local', conversationId: session.sessionId }, 1);
37
+ if (owner.resultType === 'error' && owner.error === 'no-client-found') return undefined;
38
+ if (owner.resultType !== 'success' || !owner.handledByClientId) return { ok: false, error: UNAVAILABLE };
39
+ if (policy.sessionMcpDisabled[session.sessionId]?.length) {
40
+ return { ok: false, error: 'Apply this Task’s disabled MCP settings in Codex before continuing its open desktop chat.' };
41
+ }
42
+ const state = receipt.begin();
43
+ if (state === 'accepted') return accepted;
44
+ if (state !== 'new') return refusal(state);
45
+ dispatched = true;
46
+ const response = await connection.call('thread-follower-start-turn', {
47
+ conversationId: session.sessionId,
48
+ turnStart: { request, context: { inheritThreadSettings: true } },
49
+ }, 2, owner.handledByClientId);
50
+ const turnId = response.result?.result?.turn?.id;
51
+ if (response.resultType !== 'success' || typeof turnId !== 'string' || !turnId || turnId.length > 180) {
52
+ return { ok: false, error: UNCERTAIN, delivery: 'native' };
53
+ }
54
+ receipt.accept(turnId);
55
+ return accepted;
56
+ } catch {
57
+ return { ok: false, error: dispatched ? UNCERTAIN : UNAVAILABLE,
58
+ ...(dispatched ? { delivery: 'native' as const } : {}) };
59
+ } finally { connection?.close(); }
60
+ }
61
+
62
+ function refusal(state: string): ReplyResult {
63
+ return state === 'conflict' ? { ok: false, error: 'This message ID belongs to a different Codex delivery.' }
64
+ : { ok: false, error: UNCERTAIN, delivery: 'native' };
65
+ }
66
+
67
+ function turnRequest(input: { session: SessionInfo; text: string; images: string[]; options: DeliveryOptions }, access?: string) {
68
+ const { session, text, images, options } = input;
69
+ const effort = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(options.effort ?? '') ? options.effort : undefined;
70
+ const sandboxPolicy = access === 'read-only' ? { type: 'readOnly', networkAccess: false }
71
+ : access === 'workspace' ? { type: 'workspaceWrite', writableRoots: session.cwd ? [session.cwd] : [],
72
+ networkAccess: false, excludeTmpdirEnvVar: false, excludeSlashTmp: false }
73
+ : access === 'full' ? { type: 'dangerFullAccess' } : undefined;
74
+ return { threadId: session.sessionId, clientUserMessageId: options.messageId ?? randomUUID(),
75
+ input: [{ type: 'text', text, text_elements: [] }, ...images.map(path => ({ type: 'localImage', path }))],
76
+ ...(options.model ? { model: options.model } : {}), ...(effort ? { effort } : {}),
77
+ ...(sandboxPolicy ? { sandboxPolicy } : {}) };
78
+ }
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, existsSync, fsyncSync, openSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { withStoreLock } from '../../../../packages/core/store-file-lock';
6
+ import { configDir, writePrivateFile } from '../config';
7
+
8
+ const Receipt = z.object({ digest: z.string().length(64), phase: z.enum(['sending', 'accepted']),
9
+ turnId: z.string().min(1).max(180).optional() }).strict();
10
+ type Record = z.infer<typeof Receipt>;
11
+ const hash = (value: string) => createHash('sha256').update(value).digest('hex');
12
+
13
+ /** A crashed or disconnected submission is never replayed as a new native turn. */
14
+ export class DesktopReceipt {
15
+ private readonly path: string;
16
+ private readonly digest: string;
17
+
18
+ constructor(threadId: string, messageId: string, intent: unknown) {
19
+ this.path = join(configDir(), 'codex-desktop-deliveries', hash(`${threadId}:${messageId}`) + '.json');
20
+ this.digest = hash(JSON.stringify(intent));
21
+ }
22
+
23
+ begin(): 'new' | 'accepted' | 'uncertain' | 'conflict' {
24
+ return withStoreLock(this.path, () => {
25
+ const prior = this.lookup();
26
+ if (prior) return prior;
27
+ this.save({ digest: this.digest, phase: 'sending' });
28
+ return 'new';
29
+ });
30
+ }
31
+
32
+ lookup(): 'accepted' | 'uncertain' | 'conflict' | undefined {
33
+ if (!existsSync(this.path)) return undefined;
34
+ const prior = this.read();
35
+ if (prior.digest !== this.digest) return 'conflict';
36
+ return prior.phase === 'accepted' ? 'accepted' : 'uncertain';
37
+ }
38
+
39
+ accept(turnId: string): void {
40
+ withStoreLock(this.path, () => {
41
+ const prior = this.read();
42
+ if (prior.digest !== this.digest) throw new Error('Codex delivery identity changed.');
43
+ this.save({ ...prior, phase: 'accepted', turnId });
44
+ });
45
+ }
46
+
47
+ private read(): Record { return Receipt.parse(JSON.parse(readFileSync(this.path, 'utf8'))); }
48
+ private save(record: Record): void {
49
+ writePrivateFile(this.path, JSON.stringify(record) + '\n');
50
+ for (const path of [this.path, ...(process.platform === 'win32' ? [] : [dirname(this.path)])]) {
51
+ const fd = openSync(path, 'r');
52
+ try { fsyncSync(fd); } finally { closeSync(fd); }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,25 @@
1
+ import { lstatSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ /** Only the user's existing desktop router may receive a paired controller's input. */
6
+ export function desktopSocketPath(): string | undefined {
7
+ if (process.platform === 'win32') return undefined;
8
+ const root = process.env.GRANTTAP_CODEX_DIR ?? process.env.NODVOX_CODEX_DIR
9
+ ?? process.env.CODEX_HOME ?? join(homedir(), '.codex');
10
+ const directory = join(root, 'ipc');
11
+ const socket = join(directory, 'ipc.sock');
12
+ for (const [path, isSocket] of [[root, false], [directory, false], [socket, true]] as const) {
13
+ try {
14
+ const stat = lstatSync(path);
15
+ if (stat.uid !== process.getuid?.() || (stat.mode & 0o022) !== 0
16
+ || (isSocket ? !stat.isSocket() || (stat.mode & 0o077) !== 0 : !stat.isDirectory())) {
17
+ throw new Error('The Codex desktop connection is not private.');
18
+ }
19
+ } catch (error) {
20
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
21
+ throw error;
22
+ }
23
+ }
24
+ return socket;
25
+ }
@@ -31,4 +31,4 @@ It never trusts hooks in the user's configuration.
31
31
 
32
32
  ## License
33
33
 
34
- This module is distributed under the Apache License 2.0. See the repository LICENSE.
34
+ This module is distributed under the GrantTap Commercial Source License. See the repository LICENSE.
@@ -6,5 +6,5 @@ and device pairing serialization. `../config.ts` is its public entry point.
6
6
  Behavior is covered by `tests/config.test.ts`, `tests/capabilities.test.ts`, and
7
7
  `tests/core.test.ts`.
8
8
 
9
- License: this module is distributed under the Apache License 2.0
9
+ License: this module is distributed under the GrantTap Commercial Source License
10
10
  in the repository-root `LICENSE` file.
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { isAbsolute, join, win32 } from "node:path";
3
3
  import { writePrivateFile } from "../access/write-private";
4
4
  import type {
5
5
  AgentAccess,
@@ -132,7 +132,8 @@ function parseSessionList(raw: unknown): string[] {
132
132
 
133
133
  /** Only an absolute path can be verified, so a relative one is not kept. */
134
134
  function parseEnginePath(raw: unknown): string | null {
135
- return typeof raw === "string" && raw.startsWith("/") && raw.length <= 1024 ? raw : null;
135
+ return typeof raw === "string" && raw.length <= 1024 && !/^[/\\]{2}/.test(raw)
136
+ && (isAbsolute(raw) || win32.isAbsolute(raw)) ? raw : null;
136
137
  }
137
138
 
138
139
  function parseEngineChecksum(raw: unknown): string | null {
@@ -7,4 +7,4 @@ computer-enforced chat level. Socket delivery alone is never an applied rule.
7
7
  The configuration and task-automation modules own durable admission and tests.
8
8
  Engineering contract: the Apple repository `AGENTS.md`.
9
9
 
10
- License: Apache License 2.0; see the repository LICENSE.
10
+ License: GrantTap Commercial Source License; see the repository LICENSE.
@@ -12,6 +12,7 @@ type DeliveryRecord = {
12
12
  state: "processing" | "completed";
13
13
  updatedAt: number;
14
14
  processingAcknowledged?: boolean;
15
+ rejectedError?: string;
15
16
  };
16
17
 
17
18
  export type DeliveryStart = "started" | "processing" | "completed";
@@ -89,6 +90,19 @@ export function completeDelivery(messageId: string, now = Date.now()): void {
89
90
  save(records, now);
90
91
  }
91
92
 
93
+ /** A late provider refusal remains terminal across reconnects and replayed boxes. */
94
+ export function rejectCompletedDelivery(messageId: string, error: string, now = Date.now()): void {
95
+ const records = load();
96
+ records[messageId] = { state: 'completed', updatedAt: now, rejectedError: error.slice(0, 1_000) };
97
+ save(records, now);
98
+ }
99
+
100
+ export function completedDeliveryFailure(messageId: string): string | undefined {
101
+ const record = load()[messageId];
102
+ return record?.state === 'completed' && typeof record.rejectedError === 'string'
103
+ ? record.rejectedError : undefined;
104
+ }
105
+
92
106
  /** Release a failed attempt so a relay retry can safely try again. */
93
107
  export function abandonDelivery(messageId: string, now = Date.now()): void {
94
108
  const records = load();
@@ -12,7 +12,7 @@ forwarding or promise background iPhone delivery. Self-hosted mode has no
12
12
  managed directory announcement. Settings never contain pairing secret keys.
13
13
 
14
14
  Behavior tests live in `tests/network.test.ts`. This machine runtime module is
15
- covered by the repository Apache License 2.0; the separately installed relay follows
15
+ covered by the repository GrantTap Commercial Source License; the separately installed relay follows
16
16
  its own license and customer terms.
17
17
 
18
- This runtime module is covered by the repository Apache License 2.0.
18
+ This runtime module is covered by the repository GrantTap Commercial Source License.
@@ -2,9 +2,9 @@
2
2
 
3
3
  This module owns the versioned local IPC boundary between the public machine
4
4
  runtime and the separately distributed GrantTap Engine. `engine-client.ts`
5
- implements bounded length-prefixed framing and a persistent multiplexed Unix
6
- socket. A timed-out request is never retried automatically on the hook-critical
7
- path. `engine-supervisor.ts` keeps rollout opt-in, verifies the configured
5
+ implements bounded length-prefixed framing and a persistent multiplexed local
6
+ socket or Windows named pipe. A timed-out request is never retried automatically
7
+ on the hook-critical path. `engine-supervisor.ts` verifies the configured
8
8
  binary by SHA-256 before launch, and reports explicit health without changing
9
9
  legacy hook behavior when the engine is disabled or unavailable.
10
10
  `engine-projects.ts` forwards only bounded local Project/binding records when
@@ -49,5 +49,18 @@ the durable journal.
49
49
 
50
50
  The proprietary engine source and Project database do not live in this package.
51
51
 
52
- License: this module is distributed under the Apache License 2.0
52
+ Every CLI entry point loads the persisted Engine declaration before importing
53
+ its runtime. The monitor, HTTP MCP and provider hooks therefore share activation
54
+ on macOS, Linux and Windows rather than relying on a LaunchAgent environment.
55
+ Explicit disablement and a complete explicit executable/checksum pair win over
56
+ the saved declaration. Partial environment overrides are never mixed with a
57
+ different file's checksum. The supervisor still verifies the executable's bytes
58
+ before launch. Windows clients derive a local named pipe from the normalized
59
+ configuration path; remote shares never become Engine endpoints.
60
+ The paired monitor installs the pinned managed Engine in the background when
61
+ needed. Catalog delivery does not await that download. Failed preparation retries
62
+ without losing phone questions, and a restarted helper re-admits verified local
63
+ checkouts. Explicit disablement and custom declarations are preserved.
64
+
65
+ License: this module is distributed under the GrantTap Commercial Source License
53
66
  in the repository-root `LICENSE` file.
@@ -0,0 +1,20 @@
1
+ # Engine distribution
2
+
3
+ `index.ts` exposes installation of the commercial Engine. Release
4
+ metadata is reviewed with the MCP source and pins the exact platform, size and
5
+ SHA-256. Downloads use the fixed GrantTap HTTPS origin, no redirects or cookies,
6
+ a deadline and a streaming byte limit. A download cannot choose its own hash.
7
+
8
+ Verified executables live in versioned private directories. New bytes are
9
+ written and synced to an exclusive temporary file before an atomic rename.
10
+ Failed downloads leave the previous declared Engine untouched. Setup and the
11
+ paired monitor save a new declaration only after verification.
12
+ Custom executable declarations remain available through `setup --engine`.
13
+
14
+ Provider hooks never download an executable. The catalog can connect and replay
15
+ questions independently of Engine installation and repository analysis.
16
+ The monitor prepares a missing or outdated managed Engine in the background,
17
+ retries failed downloads and recovers verified bindings once it becomes healthy.
18
+ Explicit disablement and custom executable declarations remain in force.
19
+
20
+ License: GrantTap Commercial Source License, in the repository-root `LICENSE`.