@peekdev/mcp 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +2 -2
- package/dist/mcp/host-bridge.d.ts +49 -0
- package/dist/mcp/host-bridge.d.ts.map +1 -1
- package/dist/mcp/host-bridge.js +133 -0
- package/dist/mcp/host-bridge.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +11 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/native-host/action-protocol.d.ts +13 -0
- package/dist/native-host/action-protocol.d.ts.map +1 -1
- package/dist/native-host/host-socket.d.ts +105 -0
- package/dist/native-host/host-socket.d.ts.map +1 -0
- package/dist/native-host/host-socket.js +300 -0
- package/dist/native-host/host-socket.js.map +1 -0
- package/dist/native-host/host.d.ts +11 -0
- package/dist/native-host/host.d.ts.map +1 -1
- package/dist/native-host/host.js +54 -0
- package/dist/native-host/host.js.map +1 -1
- package/dist/native-host/socket-path.d.ts +10 -0
- package/dist/native-host/socket-path.d.ts.map +1 -0
- package/dist/native-host/socket-path.js +31 -0
- package/dist/native-host/socket-path.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Read on if you're configuring the MCP server manually, building tooling against
|
|
|
30
30
|
## What this is NOT
|
|
31
31
|
|
|
32
32
|
- Not a remote MCP server. Peek is **local-only**: stdio transport over a child-process pipe. There is no HTTP listener, no SSE endpoint, no remote auth. The MCP transport spec's Streamable HTTP variant is out of scope by design.
|
|
33
|
-
- Not a write-by-default tool. Read tools are unauthenticated. The write tools (`execute_action`, `request_authorization`) are
|
|
33
|
+
- Not a write-by-default tool. Read tools are unauthenticated. The write tools (`execute_action`, `request_authorization`) are gated by the per-origin permission model (off by default) + the destructive blocklist + the audit-log writer. The cross-process IPC that delivers them to the browser native host (`LocalSocketHostBridge` ↔ `HostSocketServer` over `~/.peek/host.sock`) is now wired: at **Level 3** every action prompts the side-panel confirm banner before it runs. The real-browser MAIN-world dispatch + banner UX are covered by the Playwright E2E (`e2e/smoke.spec.ts`); the bridge, relay, dispatcher, and confirm logic are unit-tested.
|
|
34
34
|
- Not a wrapper around Chrome DevTools Protocol. The server reads recorded events from SQLite; the extension owns capture. No live `chrome.debugger` access from the MCP server.
|
|
35
35
|
|
|
36
36
|
## Manual MCP-client config
|
|
@@ -95,7 +95,7 @@ At **Level 3** every `execute_action` call prompts the user via the side-panel b
|
|
|
95
95
|
|
|
96
96
|
Every `execute_action` and `request_authorization` call is appended to `~/.peek/audit.log` (JSONL, mode 0600 — `peek audit log --json` prints it), including denied ones.
|
|
97
97
|
|
|
98
|
-
**
|
|
98
|
+
**The write-path is wired end to end:** the five-level model, the destructive blocklist, and the audit-log writer are enforced inside `peek-mcp` (observable via `~/.peek/audit.log`), and the cross-process IPC that lets `execute_action` fire a click in the browser now lands — a `LocalSocketHostBridge` (MCP process) ↔ `HostSocketServer` (native host) over `~/.peek/host.sock`, a MAIN-world action dispatcher (click/type/navigate/scroll), and a side-panel confirm banner. Both write levels are implemented: **Level 3** (act-with-confirm — every action prompts the banner) and **Level 4** (YOLO — non-destructive actions auto-allow, destructive ones still prompt via the blocklist override). Both are opt-in per origin; the default stays Level 1 (read-only). Level 2 highlight and the remaining action types are queued. The real-browser dispatch + banner are covered by the Playwright E2E (`e2e/smoke.spec.ts`).
|
|
99
99
|
|
|
100
100
|
## Database
|
|
101
101
|
|
|
@@ -77,4 +77,53 @@ export declare class RegistryBackedHostBridge implements HostBridge {
|
|
|
77
77
|
/** Test helper: reject the first pending request. */
|
|
78
78
|
rejectNext(reason: unknown): boolean;
|
|
79
79
|
}
|
|
80
|
+
/** The minimal duplex surface the bridge needs — injectable for tests. */
|
|
81
|
+
interface SocketLike {
|
|
82
|
+
write(data: string): void;
|
|
83
|
+
on(ev: string, h: (...a: unknown[]) => void): void;
|
|
84
|
+
end(): void;
|
|
85
|
+
/** Optional: decode inbound bytes as UTF-8 (real net.Socket has it). */
|
|
86
|
+
setEncoding?(encoding: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Optional: forcibly close the underlying socket (item G). Real net.Socket
|
|
89
|
+
* has it. `#reset` calls this so the orphaned socket doesn't stay open.
|
|
90
|
+
*/
|
|
91
|
+
destroy?(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Optional: detach the data/error/close listeners (item G) so a dropped
|
|
94
|
+
* socket doesn't leak its handlers. Real net.Socket (EventEmitter) has it.
|
|
95
|
+
*/
|
|
96
|
+
removeAllListeners?(): void;
|
|
97
|
+
}
|
|
98
|
+
export interface LocalSocketHostBridgeDeps {
|
|
99
|
+
/** Override the socket / named-pipe path (tests + alternate PEEK_HOME). */
|
|
100
|
+
socketPath?: string;
|
|
101
|
+
/** Injectable connection factory; defaults to `net.connect(path)`. */
|
|
102
|
+
connect?: (path: string) => SocketLike;
|
|
103
|
+
/** Injectable correlation registry (tests). */
|
|
104
|
+
registry?: RequestRegistry;
|
|
105
|
+
/** Max bytes for a single inbound line before the connection is dropped. */
|
|
106
|
+
maxLineBytes?: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The production bridge: a newline-delimited-JSON client over the local
|
|
110
|
+
* `~/.peek/host.sock` (Unix domain socket) / `\\.\pipe\peek-host` (Windows).
|
|
111
|
+
*
|
|
112
|
+
* Wire frame (both directions): one JSON object per line, `\n`-terminated.
|
|
113
|
+
* client → host: { kind: 'act.request', id, payload: HostActionRequest }
|
|
114
|
+
* host → client: { kind: 'act.response', id, payload: HostActionResponse }
|
|
115
|
+
*
|
|
116
|
+
* Correlation reuses {@link RequestRegistry} (id ↔ pending promise) exactly like
|
|
117
|
+
* {@link RegistryBackedHostBridge}; only the transport differs. The connection
|
|
118
|
+
* is opened lazily on the first request and reused. A connect throw, a socket
|
|
119
|
+
* `error`, or a timeout all resolve to a structured `deny`/`error` response —
|
|
120
|
+
* fail-closed — so the MCP tool handler never sees a raw throw and the audit
|
|
121
|
+
* log still records the attempt.
|
|
122
|
+
*/
|
|
123
|
+
export declare class LocalSocketHostBridge implements HostBridge {
|
|
124
|
+
#private;
|
|
125
|
+
constructor(deps?: LocalSocketHostBridgeDeps);
|
|
126
|
+
request(req: HostActionRequest): Promise<HostActionResponse>;
|
|
127
|
+
}
|
|
128
|
+
export {};
|
|
80
129
|
//# sourceMappingURL=host-bridge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-bridge.d.ts","sourceRoot":"","sources":["../../src/mcp/host-bridge.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"host-bridge.d.ts","sourceRoot":"","sources":["../../src/mcp/host-bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/host-bridge.js
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
//
|
|
28
28
|
// When the IPC layer lands (3d-4 or 3e) we add a concrete
|
|
29
29
|
// `LocalSocketHostBridge` here; nothing else changes.
|
|
30
|
+
import * as net from 'node:net';
|
|
30
31
|
import { RequestRegistry } from '../native-host/request-registry.js';
|
|
32
|
+
import { hostSocketPath } from '../native-host/socket-path.js';
|
|
31
33
|
// 5 minutes — longer than any plausible Level-3 banner-decision window.
|
|
32
34
|
const DEFAULT_BRIDGE_TIMEOUT_MS = 5 * 60_000;
|
|
33
35
|
/**
|
|
@@ -85,4 +87,135 @@ export class RegistryBackedHostBridge {
|
|
|
85
87
|
return this.#registry.reject(entry.id, reason);
|
|
86
88
|
}
|
|
87
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Cap for a single newline-delimited frame on the bridge's read side. A
|
|
92
|
+
* well-behaved native host never sends a frame near this; the cap is a
|
|
93
|
+
* local-DoS guard so a malformed / hostile peer can't make the bridge buffer
|
|
94
|
+
* unbounded. 1 MiB mirrors the native-messaging host→ext frame cap.
|
|
95
|
+
*/
|
|
96
|
+
const DEFAULT_MAX_LINE_BYTES = 1024 * 1024;
|
|
97
|
+
/**
|
|
98
|
+
* The production bridge: a newline-delimited-JSON client over the local
|
|
99
|
+
* `~/.peek/host.sock` (Unix domain socket) / `\\.\pipe\peek-host` (Windows).
|
|
100
|
+
*
|
|
101
|
+
* Wire frame (both directions): one JSON object per line, `\n`-terminated.
|
|
102
|
+
* client → host: { kind: 'act.request', id, payload: HostActionRequest }
|
|
103
|
+
* host → client: { kind: 'act.response', id, payload: HostActionResponse }
|
|
104
|
+
*
|
|
105
|
+
* Correlation reuses {@link RequestRegistry} (id ↔ pending promise) exactly like
|
|
106
|
+
* {@link RegistryBackedHostBridge}; only the transport differs. The connection
|
|
107
|
+
* is opened lazily on the first request and reused. A connect throw, a socket
|
|
108
|
+
* `error`, or a timeout all resolve to a structured `deny`/`error` response —
|
|
109
|
+
* fail-closed — so the MCP tool handler never sees a raw throw and the audit
|
|
110
|
+
* log still records the attempt.
|
|
111
|
+
*/
|
|
112
|
+
export class LocalSocketHostBridge {
|
|
113
|
+
#registry;
|
|
114
|
+
#path;
|
|
115
|
+
#connect;
|
|
116
|
+
#maxLineBytes;
|
|
117
|
+
#sock;
|
|
118
|
+
#buf = '';
|
|
119
|
+
constructor(deps = {}) {
|
|
120
|
+
this.#registry = deps.registry ?? new RequestRegistry();
|
|
121
|
+
this.#path = deps.socketPath ?? hostSocketPath();
|
|
122
|
+
this.#connect = deps.connect ?? ((p) => net.connect(p));
|
|
123
|
+
this.#maxLineBytes = deps.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Drop the cached socket + clear the read buffer (reconnect on next request).
|
|
127
|
+
*
|
|
128
|
+
* Item G: DESTROY the live socket + remove its listeners BEFORE nulling the
|
|
129
|
+
* reference — otherwise the orphaned socket stays open with its
|
|
130
|
+
* data/error/close listeners attached (a socket + listener leak on every
|
|
131
|
+
* reconnect, and a late event from the dead socket could still mutate state).
|
|
132
|
+
* Item H: fail every in-flight request with a structured error so a closed /
|
|
133
|
+
* errored transport rejects awaiting tool calls PROMPTLY rather than leaving
|
|
134
|
+
* them hanging until the 5-minute registry timeout.
|
|
135
|
+
*/
|
|
136
|
+
#reset(reason) {
|
|
137
|
+
const sock = this.#sock;
|
|
138
|
+
this.#sock = undefined;
|
|
139
|
+
this.#buf = '';
|
|
140
|
+
if (sock) {
|
|
141
|
+
try {
|
|
142
|
+
sock.removeAllListeners?.();
|
|
143
|
+
sock.destroy?.();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// A destroy/removeListeners throw must not mask the reset.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Item H: reject all pending requests now (fail-closed) instead of waiting
|
|
150
|
+
// out the per-request timeout.
|
|
151
|
+
this.#registry.rejectAll(new Error(reason ?? 'peek: host socket connection closed'));
|
|
152
|
+
}
|
|
153
|
+
/** Open (once) + wire the framing reader. Throws if the connect throws. */
|
|
154
|
+
#ensure() {
|
|
155
|
+
if (this.#sock)
|
|
156
|
+
return this.#sock;
|
|
157
|
+
const s = this.#connect(this.#path);
|
|
158
|
+
// Item F: decode inbound bytes as UTF-8 (matches the server side) so a
|
|
159
|
+
// multibyte char split across two reads can't corrupt a frame. Real
|
|
160
|
+
// net.Socket has setEncoding; injected fakes may not.
|
|
161
|
+
s.setEncoding?.('utf8');
|
|
162
|
+
s.on('data', (chunk) => {
|
|
163
|
+
this.#buf += String(chunk);
|
|
164
|
+
// Item F: local-DoS guard. If the buffer grows past the cap WITHOUT a
|
|
165
|
+
// frame delimiter, a hostile/malformed peer is trying to make us buffer
|
|
166
|
+
// unbounded — drop the connection + clear the buffer (in-flight requests
|
|
167
|
+
// time out via the registry → fail-closed).
|
|
168
|
+
if (this.#buf.length > this.#maxLineBytes && this.#buf.indexOf('\n') < 0) {
|
|
169
|
+
this.#reset('peek: host socket frame exceeded the line cap');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let nl;
|
|
173
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic frame-drain loop
|
|
174
|
+
while ((nl = this.#buf.indexOf('\n')) >= 0) {
|
|
175
|
+
const line = this.#buf.slice(0, nl);
|
|
176
|
+
this.#buf = this.#buf.slice(nl + 1);
|
|
177
|
+
if (!line.trim())
|
|
178
|
+
continue;
|
|
179
|
+
if (line.length > this.#maxLineBytes)
|
|
180
|
+
continue; // oversized frame — drop it
|
|
181
|
+
try {
|
|
182
|
+
const m = JSON.parse(line);
|
|
183
|
+
if (m.kind === 'act.response' && typeof m.id === 'string') {
|
|
184
|
+
this.#registry.resolve(m.id, m.payload);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Drop a malformed frame; a single bad line must not wedge the bridge.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
s.on('error', (err) => {
|
|
193
|
+
// Item H: drop the cached socket so the next request reconnects AND fail
|
|
194
|
+
// every in-flight request now (via #reset → registry.rejectAll), instead
|
|
195
|
+
// of letting them hang until the 5-minute per-request timeout.
|
|
196
|
+
this.#reset(`peek: host socket error — ${err instanceof Error ? err.message : 'connection error'}`);
|
|
197
|
+
});
|
|
198
|
+
s.on('close', () => {
|
|
199
|
+
this.#reset('peek: host socket closed');
|
|
200
|
+
});
|
|
201
|
+
this.#sock = s;
|
|
202
|
+
return s;
|
|
203
|
+
}
|
|
204
|
+
async request(req) {
|
|
205
|
+
try {
|
|
206
|
+
const sock = this.#ensure();
|
|
207
|
+
const { id, response } = this.#registry.create(req.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS);
|
|
208
|
+
sock.write(`${JSON.stringify({ kind: 'act.request', id, payload: req })}\n`);
|
|
209
|
+
return await response;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
return {
|
|
213
|
+
verdict: 'deny',
|
|
214
|
+
result: 'error',
|
|
215
|
+
approver: 'user',
|
|
216
|
+
error: err instanceof Error ? err.message : String(err),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
88
221
|
//# sourceMappingURL=host-bridge.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-bridge.js","sourceRoot":"","sources":["../../src/mcp/host-bridge.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,qDAAqD;AACrD,EAAE;AACF,oEAAoE;AACpE,2EAA2E;AAC3E,yEAAyE;AACzE,+DAA+D;AAC/D,yEAAyE;AACzE,2DAA2D;AAC3D,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,0EAA0E;AAC1E,8CAA8C;AAC9C,EAAE;AACF,yBAAyB;AACzB,uEAAuE;AACvE,gEAAgE;AAChE,6EAA6E;AAC7E,yEAAyE;AACzE,0EAA0E;AAC1E,iBAAiB;AACjB,yEAAyE;AACzE,wEAAwE;AACxE,gCAAgC;AAChC,EAAE;AACF,4DAA4D;AAC5D,wDAAwD;AAExD,OAAO,EAAE,eAAe,EAA4B,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"host-bridge.js","sourceRoot":"","sources":["../../src/mcp/host-bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ import { type CreatePeekMcpServerOptions } from './server.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Start the peek MCP stdio server and resolve when the transport closes (the
|
|
4
4
|
* client disconnected — stdin EOF). The DB handle is released on close.
|
|
5
|
+
*
|
|
6
|
+
* In MCP mode we wire the {@link LocalSocketHostBridge} so act-tool calls reach
|
|
7
|
+
* the co-running native host over ~/.peek/host.sock (replacing the
|
|
8
|
+
* MissingHostBridge default that returns "bridge not wired"). The bridge
|
|
9
|
+
* fail-closes when the socket is unavailable — every act-tool call still goes
|
|
10
|
+
* through the audit log. A caller-supplied `hostBridge` (tests) wins.
|
|
5
11
|
*/
|
|
6
12
|
export declare function runMcpServer(options?: CreatePeekMcpServerOptions): Promise<void>;
|
|
7
13
|
export { createPeekMcpServer, PEEK_MCP_TOOLS, SERVER_NAME, type CreatePeekMcpServerOptions, type PeekMcpServer, } from './server.js';
|
package/dist/mcp/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,0BAA0B,EAAuB,MAAM,aAAa,CAAC;AAEnF;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,0BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB1F;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,KAAK,0BAA0B,EAC/B,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
// exposes in v1. Invoked from src/index.ts when `peek-mcp` runs without
|
|
4
4
|
// --native-host and without a chrome-extension:// origin arg.
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { LocalSocketHostBridge } from './host-bridge.js';
|
|
6
7
|
import { createPeekMcpServer } from './server.js';
|
|
7
8
|
/**
|
|
8
9
|
* Start the peek MCP stdio server and resolve when the transport closes (the
|
|
9
10
|
* client disconnected — stdin EOF). The DB handle is released on close.
|
|
11
|
+
*
|
|
12
|
+
* In MCP mode we wire the {@link LocalSocketHostBridge} so act-tool calls reach
|
|
13
|
+
* the co-running native host over ~/.peek/host.sock (replacing the
|
|
14
|
+
* MissingHostBridge default that returns "bridge not wired"). The bridge
|
|
15
|
+
* fail-closes when the socket is unavailable — every act-tool call still goes
|
|
16
|
+
* through the audit log. A caller-supplied `hostBridge` (tests) wins.
|
|
10
17
|
*/
|
|
11
18
|
export async function runMcpServer(options = {}) {
|
|
12
|
-
const peek = createPeekMcpServer(
|
|
19
|
+
const peek = createPeekMcpServer({
|
|
20
|
+
hostBridge: new LocalSocketHostBridge(),
|
|
21
|
+
...options,
|
|
22
|
+
});
|
|
13
23
|
const transport = new StdioServerTransport();
|
|
14
24
|
// Resolve when the client disconnects (stdin EOF). McpServer.connect
|
|
15
25
|
// (Protocol.connect) PRESERVES and chains a pre-existing transport.onclose:
|
package/dist/mcp/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AACxE,8DAA8D;AAE9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAmC,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEnF
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AACxE,8DAA8D;AAE9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAmC,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEnF;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAsC,EAAE;IACzE,MAAM,IAAI,GAAG,mBAAmB,CAAC;QAC/B,UAAU,EAAE,IAAI,qBAAqB,EAAE;QACvC,GAAG,OAAO;KACX,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,qEAAqE;IACrE,4EAA4E;IAC5E,yEAAyE;IACzE,4EAA4E;IAC5E,8EAA8E;IAC9E,iCAAiC;IACjC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAC3C,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;YACvB,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAErC,8EAA8E;IAC9E,uEAAuE;IACvE,MAAM,MAAM,CAAC;AACf,CAAC;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,WAAW,GAGZ,MAAM,aAAa,CAAC"}
|
|
@@ -19,6 +19,12 @@ export interface ActionResultMessage {
|
|
|
19
19
|
details?: unknown;
|
|
20
20
|
/** Error message when `result === 'error'` or 'denied'. */
|
|
21
21
|
error?: string;
|
|
22
|
+
/**
|
|
23
|
+
* A one-shot confirm token issued on a `request_authorization` reply. The
|
|
24
|
+
* relay carries it back to the MCP process so the AI can pass it to a later
|
|
25
|
+
* `execute_action` (mirrors the extension-side ActionResultMessage).
|
|
26
|
+
*/
|
|
27
|
+
confirmToken?: string;
|
|
22
28
|
}
|
|
23
29
|
/** host → SW: please execute / authorize this action. */
|
|
24
30
|
export interface ActionRequestMessage {
|
|
@@ -36,6 +42,13 @@ export interface ActionRequestMessage {
|
|
|
36
42
|
};
|
|
37
43
|
/** Optional pinning to a specific tab id (SW picks active when omitted). */
|
|
38
44
|
tabId?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Pre-issued one-shot token from a prior `request_authorization` call. When
|
|
47
|
+
* present and valid (matching this request's sessionId + action.type), the SW
|
|
48
|
+
* consumes it and skips the side-panel banner. NULL/undefined → no token; the
|
|
49
|
+
* banner runs.
|
|
50
|
+
*/
|
|
51
|
+
confirmToken?: string;
|
|
39
52
|
}
|
|
40
53
|
/** SW → host: the banner is now visible to the user (timing signal). */
|
|
41
54
|
export interface ActionConfirmShownMessage {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-protocol.d.ts","sourceRoot":"","sources":["../../src/native-host/action-protocol.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,IAAI,EAAE,gBAAgB,GAAG,uBAAuB,CAAC;IACjD,mEAAmE;IACnE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,oEAAoE;IACpE,MAAM,EAAE,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC;IAClC,oDAAoD;IACpD,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,cAAc,CAAC;IACvD,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"action-protocol.d.ts","sourceRoot":"","sources":["../../src/native-host/action-protocol.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,IAAI,EAAE,gBAAgB,GAAG,uBAAuB,CAAC;IACjD,mEAAmE;IACnE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,oEAAoE;IACpE,MAAM,EAAE,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC;IAClC,oDAAoD;IACpD,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,cAAc,CAAC;IACvD,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,gBAAgB,GAAG,uBAAuB,CAAC;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,MAAM,EAAE;QACN,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;QACvB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AACnD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ActionConfirmShownMessage, ActionRequestMessage, ActionResultMessage } from './action-protocol.js';
|
|
2
|
+
import { type LoadedPolicy } from './policy.js';
|
|
3
|
+
/**
|
|
4
|
+
* Item E + I: unlink a Unix-domain-socket file ONLY when it's safe to do so —
|
|
5
|
+
* the path is a socket inode (never a regular file the user owns). This is the
|
|
6
|
+
* pure filesystem guard; the LIVE-vs-stale decision (item I) is made by the
|
|
7
|
+
* caller via {@link probeSocketAlive} BEFORE calling this, because the mere
|
|
8
|
+
* existence of a socket inode does NOT mean it's stale — a live host could be
|
|
9
|
+
* listening on it. Clobbering a live host's socket would silently break it.
|
|
10
|
+
*
|
|
11
|
+
* A missing path is a no-op. Windows named pipes aren't filesystem paths, so
|
|
12
|
+
* this is skipped there. Best-effort: any stat/unlink error is swallowed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanupStaleSocket(path: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Item I: probe whether a LIVE server is listening at `path`. Connects with a
|
|
17
|
+
* short timeout: a successful connect → a live host owns the socket (do NOT
|
|
18
|
+
* unlink it); a refused/ENOENT/timeout → the inode is stale (safe to unlink and
|
|
19
|
+
* retry the bind). Windows named pipes are probed the same way (net.connect
|
|
20
|
+
* accepts the pipe path). Resolves false on any error so a probe failure can't
|
|
21
|
+
* wedge startup.
|
|
22
|
+
*/
|
|
23
|
+
export declare function probeSocketAlive(path: string, timeoutMs?: number): Promise<boolean>;
|
|
24
|
+
/** Minimal duplex surface a connection must provide — injectable for tests. */
|
|
25
|
+
export interface ConnectionLike {
|
|
26
|
+
write(data: string): void;
|
|
27
|
+
on(ev: string, h: (...a: unknown[]) => void): void;
|
|
28
|
+
/** Optional: forcibly close the connection (item J — over-cap line drop). */
|
|
29
|
+
destroy?(): void;
|
|
30
|
+
/** Optional: gracefully end the connection (fallback when destroy is absent). */
|
|
31
|
+
end?(): void;
|
|
32
|
+
}
|
|
33
|
+
/** Minimal server surface — injectable for tests (real impl is net.Server). */
|
|
34
|
+
export interface NetServerLike {
|
|
35
|
+
listen(): void;
|
|
36
|
+
close(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Item I: subscribe to the server's async 'error' event (EADDRINUSE/EACCES are
|
|
39
|
+
* emitted here, NOT thrown from listen()). Optional so existing fakes that
|
|
40
|
+
* never error don't need it.
|
|
41
|
+
*/
|
|
42
|
+
on?(ev: 'error', handler: (err: Error & {
|
|
43
|
+
code?: string;
|
|
44
|
+
}) => void): void;
|
|
45
|
+
}
|
|
46
|
+
export interface HostSocketServerDeps {
|
|
47
|
+
/** Forward an `action.request` to the SW over the native port. */
|
|
48
|
+
postToSw(message: ActionRequestMessage): void;
|
|
49
|
+
/** Read ~/.peek/policy.json (per request — cheap). Defaults to {@link loadPolicy}. */
|
|
50
|
+
loadPolicy?: () => LoadedPolicy;
|
|
51
|
+
/** Generate a fresh native-port requestId. Defaults to crypto.randomUUID. */
|
|
52
|
+
generateRequestId?: () => string;
|
|
53
|
+
/** Override the socket / named-pipe path. */
|
|
54
|
+
socketPath?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Build the underlying server given the per-connection handler. Defaults to
|
|
57
|
+
* `net.createServer`; tests inject a fake that drives connections directly.
|
|
58
|
+
*/
|
|
59
|
+
createServer?: (onConnection: (conn: ConnectionLike) => void) => NetServerLike;
|
|
60
|
+
/**
|
|
61
|
+
* Item J: max bytes for a single inbound line before the connection is
|
|
62
|
+
* dropped. Defaults to 1 MiB (matches the bridge side).
|
|
63
|
+
*/
|
|
64
|
+
maxLineBytes?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Item I: probe whether a LIVE server owns the socket path. Defaults to
|
|
67
|
+
* {@link probeSocketAlive}; injectable so the bind-failure policy is testable.
|
|
68
|
+
*/
|
|
69
|
+
probeSocketAlive?: (path: string) => Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Item I: unlink the (confirmed-stale) socket inode. Defaults to
|
|
72
|
+
* {@link cleanupStaleSocket}; injectable for tests.
|
|
73
|
+
*/
|
|
74
|
+
unlinkSocket?: (path: string) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Item I: called when the socket fails to bind even after the stale-unlink
|
|
77
|
+
* retry (e.g. a LIVE owner, or EACCES). The caller (startNativeHost) uses
|
|
78
|
+
* this to degrade — set `socketServer = undefined` — instead of crashing.
|
|
79
|
+
*/
|
|
80
|
+
onListenError?: (err: Error) => void;
|
|
81
|
+
}
|
|
82
|
+
export declare class HostSocketServer {
|
|
83
|
+
#private;
|
|
84
|
+
constructor(deps: HostSocketServerDeps);
|
|
85
|
+
/**
|
|
86
|
+
* Start listening for MCP-process connections. Item I: bind failures
|
|
87
|
+
* (EADDRINUSE/EACCES) surface ASYNC via the server 'error' event, not a throw,
|
|
88
|
+
* so we attach an error handler and run the stale-vs-live policy:
|
|
89
|
+
* • EADDRINUSE + probe says LIVE → another host owns it: report + stay down.
|
|
90
|
+
* • EADDRINUSE + probe says stale → unlink + retry listen ONCE.
|
|
91
|
+
* • any other bind error (e.g. EACCES) or a second EADDRINUSE → report + down.
|
|
92
|
+
* "Report" calls onListenError so startNativeHost degrades gracefully.
|
|
93
|
+
*/
|
|
94
|
+
listen(): void;
|
|
95
|
+
/** Stop the server (best-effort). */
|
|
96
|
+
close(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Inbound from the SW over the native port. For a terminal `action.result`,
|
|
99
|
+
* find the originating connection by requestId and write back the mapped
|
|
100
|
+
* `act.response`. `action.confirm.shown` is a non-terminal timing signal —
|
|
101
|
+
* the verdict still arrives in a later `action.result` — so we drop it here.
|
|
102
|
+
*/
|
|
103
|
+
onSwMessage(message: ActionResultMessage | ActionConfirmShownMessage): void;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=host-socket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-socket.d.ts","sourceRoot":"","sources":["../../src/native-host/host-socket.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EACV,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,YAAY,EAAc,MAAM,aAAa,CAAC;AAG5D;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAUrD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBhF;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IACnD,6EAA6E;IAC7E,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,iFAAiF;IACjF,GAAG,CAAC,IAAI,IAAI,CAAC;CACd;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;IACd;;;;OAIG;IACH,EAAE,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CAC3E;AAKD,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC9C,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,YAAY,CAAC;IAChC,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,MAAM,CAAC;IACjC,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,KAAK,aAAa,CAAC;IAC/E;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CACtC;AA6BD,qBAAa,gBAAgB;;gBAUf,IAAI,EAAE,oBAAoB;IAiCtC;;;;;;;;OAQG;IACH,MAAM,IAAI,IAAI;IAoCd,qCAAqC;IACrC,KAAK,IAAI,IAAI;IAMb;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,yBAAyB,GAAG,IAAI;CAkG5E"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Native-host-side IPC relay (Task 3.24 IPC layer).
|
|
2
|
+
//
|
|
3
|
+
// The MCP-server process talks to this server over the local socket
|
|
4
|
+
// (`~/.peek/host.sock`) using the {@link LocalSocketHostBridge}. This server
|
|
5
|
+
// runs INSIDE the native-host process — the one that owns the
|
|
6
|
+
// `chrome.runtime.connectNative` port to the service worker. It relays:
|
|
7
|
+
//
|
|
8
|
+
// socket act.request → native-port action.request (host → SW)
|
|
9
|
+
// native-port action.result / action.confirm.shown → socket act.response
|
|
10
|
+
//
|
|
11
|
+
// Correlation has TWO id spaces that must not be conflated:
|
|
12
|
+
// • the socket wire `id` (the LocalSocketHostBridge's RequestRegistry id) —
|
|
13
|
+
// the MCP process correlates `act.response` back to its awaiting tool call.
|
|
14
|
+
// • the native-port `requestId` (UUID) — the SW echoes it on its reply so we
|
|
15
|
+
// find the originating socket connection.
|
|
16
|
+
// We hold a `Map<requestId, { conn, wireId }>` so an `action.result` resolves
|
|
17
|
+
// to the right connection AND echoes the right wire id.
|
|
18
|
+
//
|
|
19
|
+
// Security / robustness: per-connection newline-JSON framing (reused shape from
|
|
20
|
+
// the bridge); a malformed frame is dropped, never crashes the loop; an
|
|
21
|
+
// `action.result` for an unknown requestId is dropped (a stale reply after a
|
|
22
|
+
// timeout must not throw). The policy deltas are loaded per request (the file
|
|
23
|
+
// is tiny) and forwarded so the SW's destructive matcher merges them.
|
|
24
|
+
import { statSync, unlinkSync } from 'node:fs';
|
|
25
|
+
import * as net from 'node:net';
|
|
26
|
+
import { platform } from 'node:os';
|
|
27
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
28
|
+
import { loadPolicy } from './policy.js';
|
|
29
|
+
import { hostSocketPath } from './socket-path.js';
|
|
30
|
+
/**
|
|
31
|
+
* Item E + I: unlink a Unix-domain-socket file ONLY when it's safe to do so —
|
|
32
|
+
* the path is a socket inode (never a regular file the user owns). This is the
|
|
33
|
+
* pure filesystem guard; the LIVE-vs-stale decision (item I) is made by the
|
|
34
|
+
* caller via {@link probeSocketAlive} BEFORE calling this, because the mere
|
|
35
|
+
* existence of a socket inode does NOT mean it's stale — a live host could be
|
|
36
|
+
* listening on it. Clobbering a live host's socket would silently break it.
|
|
37
|
+
*
|
|
38
|
+
* A missing path is a no-op. Windows named pipes aren't filesystem paths, so
|
|
39
|
+
* this is skipped there. Best-effort: any stat/unlink error is swallowed.
|
|
40
|
+
*/
|
|
41
|
+
export function cleanupStaleSocket(path) {
|
|
42
|
+
if (platform() === 'win32')
|
|
43
|
+
return; // named pipe — not a filesystem inode
|
|
44
|
+
try {
|
|
45
|
+
const st = statSync(path);
|
|
46
|
+
if (!st.isSocket())
|
|
47
|
+
return; // never clobber a regular file / dir
|
|
48
|
+
unlinkSync(path);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ENOENT (nothing there) or any other stat/unlink error — leave it for the
|
|
52
|
+
// bind to report. Don't throw; cleanup is best-effort.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Item I: probe whether a LIVE server is listening at `path`. Connects with a
|
|
57
|
+
* short timeout: a successful connect → a live host owns the socket (do NOT
|
|
58
|
+
* unlink it); a refused/ENOENT/timeout → the inode is stale (safe to unlink and
|
|
59
|
+
* retry the bind). Windows named pipes are probed the same way (net.connect
|
|
60
|
+
* accepts the pipe path). Resolves false on any error so a probe failure can't
|
|
61
|
+
* wedge startup.
|
|
62
|
+
*/
|
|
63
|
+
export function probeSocketAlive(path, timeoutMs = 500) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let settled = false;
|
|
66
|
+
const done = (alive) => {
|
|
67
|
+
if (settled)
|
|
68
|
+
return;
|
|
69
|
+
settled = true;
|
|
70
|
+
try {
|
|
71
|
+
client.destroy();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
resolve(alive);
|
|
77
|
+
};
|
|
78
|
+
const client = net.connect(path);
|
|
79
|
+
client.setTimeout?.(timeoutMs);
|
|
80
|
+
client.on('connect', () => done(true)); // a live server accepted us
|
|
81
|
+
client.on('error', () => done(false)); // ECONNREFUSED / ENOENT → stale
|
|
82
|
+
client.on('timeout', () => done(false));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Default cap for a single inbound newline-delimited frame (item J). 1 MiB. */
|
|
86
|
+
const DEFAULT_MAX_LINE_BYTES = 1024 * 1024;
|
|
87
|
+
/**
|
|
88
|
+
* Map a terminal {@link ActionResultMessage} to the wire `payload` the bridge
|
|
89
|
+
* expects (a {@link HostActionResponse}). Drops the wire-protocol envelope
|
|
90
|
+
* fields (`type`, `requestId`, `tool`) and keeps the verdict/result data —
|
|
91
|
+
* INCLUDING `confirmToken` when the SW issued one (request_authorization).
|
|
92
|
+
*/
|
|
93
|
+
function toResponsePayload(result) {
|
|
94
|
+
const payload = {
|
|
95
|
+
verdict: result.verdict,
|
|
96
|
+
result: result.result,
|
|
97
|
+
approver: result.approver,
|
|
98
|
+
};
|
|
99
|
+
if (result.approvalMs !== undefined)
|
|
100
|
+
payload.approvalMs = result.approvalMs;
|
|
101
|
+
if (result.destructiveTerm !== undefined)
|
|
102
|
+
payload.destructiveTerm = result.destructiveTerm;
|
|
103
|
+
if (result.details !== undefined)
|
|
104
|
+
payload.details = result.details;
|
|
105
|
+
if (result.error !== undefined)
|
|
106
|
+
payload.error = result.error;
|
|
107
|
+
if (result.confirmToken !== undefined)
|
|
108
|
+
payload.confirmToken = result.confirmToken;
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
export class HostSocketServer {
|
|
112
|
+
#deps;
|
|
113
|
+
#inFlight = new Map();
|
|
114
|
+
#server;
|
|
115
|
+
/** Whether we've already done the one stale-unlink retry (item I). */
|
|
116
|
+
#retriedAfterUnlink = false;
|
|
117
|
+
constructor(deps) {
|
|
118
|
+
this.#deps = {
|
|
119
|
+
postToSw: deps.postToSw,
|
|
120
|
+
loadPolicy: deps.loadPolicy ?? (() => loadPolicy()),
|
|
121
|
+
generateRequestId: deps.generateRequestId ?? (() => globalThis.crypto.randomUUID()),
|
|
122
|
+
socketPath: deps.socketPath ?? hostSocketPath(),
|
|
123
|
+
maxLineBytes: deps.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES,
|
|
124
|
+
probeSocketAlive: deps.probeSocketAlive ?? ((p) => probeSocketAlive(p)),
|
|
125
|
+
unlinkSocket: deps.unlinkSocket ?? ((p) => cleanupStaleSocket(p)),
|
|
126
|
+
...(deps.onListenError ? { onListenError: deps.onListenError } : {}),
|
|
127
|
+
createServer: deps.createServer ??
|
|
128
|
+
((onConnection) => {
|
|
129
|
+
const server = net.createServer((socket) => {
|
|
130
|
+
socket.setEncoding('utf8');
|
|
131
|
+
onConnection(socket);
|
|
132
|
+
});
|
|
133
|
+
let errorHandler;
|
|
134
|
+
server.on('error', (err) => errorHandler?.(err));
|
|
135
|
+
return {
|
|
136
|
+
// Item I: do NOT blindly unlink before binding (that could clobber a
|
|
137
|
+
// LIVE host's socket). Bind directly; if it emits EADDRINUSE the
|
|
138
|
+
// listen() orchestration probes live-vs-stale and only then unlinks.
|
|
139
|
+
listen: () => server.listen(this.#deps.socketPath),
|
|
140
|
+
close: () => server.close(),
|
|
141
|
+
on: (_ev, handler) => {
|
|
142
|
+
errorHandler = handler;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Start listening for MCP-process connections. Item I: bind failures
|
|
150
|
+
* (EADDRINUSE/EACCES) surface ASYNC via the server 'error' event, not a throw,
|
|
151
|
+
* so we attach an error handler and run the stale-vs-live policy:
|
|
152
|
+
* • EADDRINUSE + probe says LIVE → another host owns it: report + stay down.
|
|
153
|
+
* • EADDRINUSE + probe says stale → unlink + retry listen ONCE.
|
|
154
|
+
* • any other bind error (e.g. EACCES) or a second EADDRINUSE → report + down.
|
|
155
|
+
* "Report" calls onListenError so startNativeHost degrades gracefully.
|
|
156
|
+
*/
|
|
157
|
+
listen() {
|
|
158
|
+
if (this.#server)
|
|
159
|
+
return;
|
|
160
|
+
const server = this.#deps.createServer((conn) => this.#onConnection(conn));
|
|
161
|
+
this.#server = server;
|
|
162
|
+
server.on?.('error', (err) => {
|
|
163
|
+
void this.#handleListenError(err);
|
|
164
|
+
});
|
|
165
|
+
server.listen();
|
|
166
|
+
}
|
|
167
|
+
async #handleListenError(err) {
|
|
168
|
+
// Only EADDRINUSE is recoverable via the stale-unlink dance; everything else
|
|
169
|
+
// (EACCES, etc.) is reported as-is.
|
|
170
|
+
if (err.code !== 'EADDRINUSE' || this.#retriedAfterUnlink) {
|
|
171
|
+
this.#reportListenFailure(err);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const alive = await this.#deps.probeSocketAlive(this.#deps.socketPath).catch(() => false);
|
|
175
|
+
if (alive) {
|
|
176
|
+
// A live host already owns this socket — DO NOT clobber it. Degrade.
|
|
177
|
+
this.#reportListenFailure(err);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Stale inode: unlink it and retry the bind exactly once.
|
|
181
|
+
this.#retriedAfterUnlink = true;
|
|
182
|
+
this.#deps.unlinkSocket(this.#deps.socketPath);
|
|
183
|
+
// A short beat so the unlink settles before rebind (mostly belt-and-braces).
|
|
184
|
+
await delay(0);
|
|
185
|
+
this.#server?.listen();
|
|
186
|
+
}
|
|
187
|
+
#reportListenFailure(err) {
|
|
188
|
+
this.#server = undefined;
|
|
189
|
+
this.#deps.onListenError?.(err);
|
|
190
|
+
}
|
|
191
|
+
/** Stop the server (best-effort). */
|
|
192
|
+
close() {
|
|
193
|
+
this.#server?.close();
|
|
194
|
+
this.#server = undefined;
|
|
195
|
+
this.#inFlight.clear();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Inbound from the SW over the native port. For a terminal `action.result`,
|
|
199
|
+
* find the originating connection by requestId and write back the mapped
|
|
200
|
+
* `act.response`. `action.confirm.shown` is a non-terminal timing signal —
|
|
201
|
+
* the verdict still arrives in a later `action.result` — so we drop it here.
|
|
202
|
+
*/
|
|
203
|
+
onSwMessage(message) {
|
|
204
|
+
if (message.type !== 'action.result')
|
|
205
|
+
return; // confirm.shown: timing only
|
|
206
|
+
const inFlight = this.#inFlight.get(message.requestId);
|
|
207
|
+
if (!inFlight)
|
|
208
|
+
return; // stale reply after a timeout — drop, don't throw
|
|
209
|
+
this.#inFlight.delete(message.requestId);
|
|
210
|
+
const frame = {
|
|
211
|
+
kind: 'act.response',
|
|
212
|
+
id: inFlight.wireId,
|
|
213
|
+
payload: toResponsePayload(message),
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
inFlight.conn.write(`${JSON.stringify(frame)}\n`);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// The MCP process closed the socket mid-flight; its tool call will time
|
|
220
|
+
// out. Nothing else to do here.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
#onConnection(conn) {
|
|
224
|
+
let buf = '';
|
|
225
|
+
let dropped = false;
|
|
226
|
+
conn.on('data', (chunk) => {
|
|
227
|
+
if (dropped)
|
|
228
|
+
return; // connection already torn down for an over-cap line
|
|
229
|
+
buf += String(chunk);
|
|
230
|
+
// Item J: local-DoS guard (mirrors the bridge). If the buffer grows past
|
|
231
|
+
// the cap WITHOUT a frame delimiter, a hostile/malformed peer is trying to
|
|
232
|
+
// make us buffer unbounded — drop the connection + clear the buffer.
|
|
233
|
+
if (buf.length > this.#deps.maxLineBytes && buf.indexOf('\n') < 0) {
|
|
234
|
+
dropped = true;
|
|
235
|
+
buf = '';
|
|
236
|
+
try {
|
|
237
|
+
conn.destroy?.();
|
|
238
|
+
conn.end?.();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// best-effort teardown
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
let nl;
|
|
246
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic frame-drain loop
|
|
247
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
248
|
+
const line = buf.slice(0, nl);
|
|
249
|
+
buf = buf.slice(nl + 1);
|
|
250
|
+
if (!line.trim())
|
|
251
|
+
continue;
|
|
252
|
+
if (line.length > this.#deps.maxLineBytes)
|
|
253
|
+
continue; // oversized framed line — drop it
|
|
254
|
+
this.#handleFrame(conn, line);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// A connection error / close leaves any of its in-flight entries dangling;
|
|
258
|
+
// they'll never be resolved (the MCP tool call times out). Drop them so the
|
|
259
|
+
// map doesn't grow unbounded.
|
|
260
|
+
const drop = () => {
|
|
261
|
+
for (const [requestId, entry] of this.#inFlight.entries()) {
|
|
262
|
+
if (entry.conn === conn)
|
|
263
|
+
this.#inFlight.delete(requestId);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
conn.on('error', drop);
|
|
267
|
+
conn.on('close', drop);
|
|
268
|
+
conn.on('end', drop);
|
|
269
|
+
}
|
|
270
|
+
#handleFrame(conn, line) {
|
|
271
|
+
let frame;
|
|
272
|
+
try {
|
|
273
|
+
frame = JSON.parse(line);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return; // malformed — drop, keep the loop alive
|
|
277
|
+
}
|
|
278
|
+
if (frame.kind !== 'act.request' || typeof frame.id !== 'string')
|
|
279
|
+
return;
|
|
280
|
+
const payload = frame.payload;
|
|
281
|
+
if (!payload || payload.tool === undefined || payload.action === undefined)
|
|
282
|
+
return;
|
|
283
|
+
const requestId = this.#deps.generateRequestId();
|
|
284
|
+
this.#inFlight.set(requestId, { conn, wireId: frame.id });
|
|
285
|
+
const policy = this.#deps.loadPolicy().destructiveTerms;
|
|
286
|
+
const request = {
|
|
287
|
+
type: 'action.request',
|
|
288
|
+
requestId,
|
|
289
|
+
tool: payload.tool,
|
|
290
|
+
sessionId: payload.sessionId ?? '',
|
|
291
|
+
action: payload.action,
|
|
292
|
+
client: payload.client ?? 'unknown',
|
|
293
|
+
policy: { add: policy.add, remove: policy.remove },
|
|
294
|
+
...(payload.tabId !== undefined ? { tabId: payload.tabId } : {}),
|
|
295
|
+
...(payload.confirmToken !== undefined ? { confirmToken: payload.confirmToken } : {}),
|
|
296
|
+
};
|
|
297
|
+
this.#deps.postToSw(request);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=host-socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-socket.js","sourceRoot":"","sources":["../../src/native-host/host-socket.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,oEAAoE;AACpE,6EAA6E;AAC7E,8DAA8D;AAC9D,wEAAwE;AACxE,EAAE;AACF,uEAAuE;AACvE,6EAA6E;AAC7E,EAAE;AACF,4DAA4D;AAC5D,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,8CAA8C;AAC9C,8EAA8E;AAC9E,wDAAwD;AACxD,EAAE;AACF,gFAAgF;AAChF,wEAAwE;AACxE,6EAA6E;AAC7E,8EAA8E;AAC9E,sEAAsE;AAEtE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAM3D,OAAO,EAAqB,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,QAAQ,EAAE,KAAK,OAAO;QAAE,OAAO,CAAC,sCAAsC;IAC1E,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE;YAAE,OAAO,CAAC,qCAAqC;QACjE,UAAU,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,uDAAuD;IACzD,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,SAAS,GAAG,GAAG;IAC5D,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QACtC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE;YAC9B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QACF,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,CAAC;QAC/B,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,4BAA4B;QACpE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,gCAAgC;QACvE,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC;AAwBD,gFAAgF;AAChF,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAAC;AA8C3C;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,MAA2B;IACpD,MAAM,OAAO,GAA4B;QACvC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;IACF,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IAC5E,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS;QAAE,OAAO,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,CAAC;IAC3F,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IACnE,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC7D,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS;QAAE,OAAO,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IAClF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,OAAO,gBAAgB;IAClB,KAAK,CAGZ;IACO,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IACjD,OAAO,CAA4B;IACnC,sEAAsE;IACtE,mBAAmB,GAAG,KAAK,CAAC;IAE5B,YAAY,IAA0B;QACpC,IAAI,CAAC,KAAK,GAAG;YACX,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC;YACnD,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACnF,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,cAAc,EAAE;YAC/C,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,sBAAsB;YACzD,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;YACvE,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;YACjE,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,YAAY,EACV,IAAI,CAAC,YAAY;gBACjB,CAAC,CAAC,YAAY,EAAE,EAAE;oBAChB,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE;wBACzC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;wBAC3B,YAAY,CAAC,MAAmC,CAAC,CAAC;oBACpD,CAAC,CAAC,CAAC;oBACH,IAAI,YAAoE,CAAC;oBACzE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,EAAE,CAAC,GAAgC,CAAC,CAAC,CAAC;oBAC9E,OAAO;wBACL,qEAAqE;wBACrE,iEAAiE;wBACjE,qEAAqE;wBACrE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;wBAClD,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;wBAC3B,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE;4BACnB,YAAY,GAAG,OAAO,CAAC;wBACzB,CAAC;qBACF,CAAC;gBACJ,CAAC,CAAC;SACL,CAAC;IACJ,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM;QACJ,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3E,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC3B,KAAK,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,EAAE,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,GAA8B;QACrD,6EAA6E;QAC7E,oCAAoC;QACpC,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC1D,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QAC1F,IAAI,KAAK,EAAE,CAAC;YACV,qEAAqE;YACrE,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO;QACT,CAAC;QACD,0DAA0D;QAC1D,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/C,6EAA6E;QAC7E,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;QACf,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IACzB,CAAC;IAED,oBAAoB,CAAC,GAAU;QAC7B,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QACzB,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,qCAAqC;IACrC,KAAK;QACH,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QACzB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,OAAwD;QAClE,IAAI,OAAO,CAAC,IAAI,KAAK,eAAe;YAAE,OAAO,CAAC,6BAA6B;QAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACvD,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,kDAAkD;QACzE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG;YACZ,IAAI,EAAE,cAAc;YACpB,EAAE,EAAE,QAAQ,CAAC,MAAM;YACnB,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC;SACpC,CAAC;QACF,IAAI,CAAC;YACH,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;YACxE,gCAAgC;QAClC,CAAC;IACH,CAAC;IAED,aAAa,CAAC,IAAoB;QAChC,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAc,EAAE,EAAE;YACjC,IAAI,OAAO;gBAAE,OAAO,CAAC,oDAAoD;YACzE,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;YACrB,yEAAyE;YACzE,2EAA2E;YAC3E,qEAAqE;YACrE,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClE,OAAO,GAAG,IAAI,CAAC;gBACf,GAAG,GAAG,EAAE,CAAC;gBACT,IAAI,CAAC;oBACH,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;oBACjB,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;gBACf,CAAC;gBAAC,MAAM,CAAC;oBACP,uBAAuB;gBACzB,CAAC;gBACD,OAAO;YACT,CAAC;YACD,IAAI,EAAU,CAAC;YACf,iFAAiF;YACjF,OAAO,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;oBAAE,SAAS;gBAC3B,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY;oBAAE,SAAS,CAAC,kCAAkC;gBACvF,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;QACH,CAAC,CAAC,CAAC;QACH,2EAA2E;QAC3E,4EAA4E;QAC5E,8BAA8B;QAC9B,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC1D,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;oBAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC;QACF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvB,CAAC;IAED,YAAY,CAAC,IAAoB,EAAE,IAAY;QAC7C,IAAI,KAAwD,CAAC;QAC7D,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,wCAAwC;QAClD,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;YAAE,OAAO;QACzE,MAAM,OAAO,GAAG,KAAK,CAAC,OAST,CAAC;QACd,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO;QAEnF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC;QACjD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAE1D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,gBAAgB,CAAC;QACxD,MAAM,OAAO,GAAyB;YACpC,IAAI,EAAE,gBAAgB;YACtB,SAAS;YACT,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;YAClC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,SAAS;YACnC,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;YAClD,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtF,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -20,6 +20,17 @@ export interface NativeHostOptions {
|
|
|
20
20
|
readonly input?: Readable;
|
|
21
21
|
/** Override the output stream (default: process.stdout). */
|
|
22
22
|
readonly output?: Writable;
|
|
23
|
+
/**
|
|
24
|
+
* Override the IPC socket / named-pipe path the {@link HostSocketServer}
|
|
25
|
+
* listens on (tests + alternate PEEK_HOME). Defaults to ~/.peek/host.sock.
|
|
26
|
+
*/
|
|
27
|
+
readonly socketPath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* When false, skip starting the {@link HostSocketServer}. Defaults to true.
|
|
30
|
+
* Tests that only exercise the ingest/handshake path set this false to avoid
|
|
31
|
+
* binding a real socket.
|
|
32
|
+
*/
|
|
33
|
+
readonly startSocketServer?: boolean;
|
|
23
34
|
}
|
|
24
35
|
/**
|
|
25
36
|
* Start the native messaging host: open the DB (running migrations) and begin
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/native-host/host.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/native-host/host.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQtD,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,mCAAmC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAaD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAmIjF"}
|
package/dist/native-host/host.js
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
// network.append / shadow.report). The act-tool action-protocol dispatch
|
|
10
10
|
// (request/result correlation through the SW) lands alongside the IPC bridge.
|
|
11
11
|
import { openDb, peekHomeDir, schemaVersion } from '../db/open.js';
|
|
12
|
+
import { HostSocketServer } from './host-socket.js';
|
|
12
13
|
import { ingest } from './ingest.js';
|
|
14
|
+
import { hostSocketPath } from './socket-path.js';
|
|
13
15
|
import { readMessages, writeMessage } from './transport.js';
|
|
16
|
+
/** Message types the SW sends back on the action-request correlation channel. */
|
|
17
|
+
const ACTION_REPLY_TYPES = new Set(['action.result', 'action.confirm.shown']);
|
|
14
18
|
/** Message-type strings the ingest handler claims. */
|
|
15
19
|
const INGEST_TYPES = new Set([
|
|
16
20
|
'session.append',
|
|
@@ -35,6 +39,47 @@ export function startNativeHost(options = {}) {
|
|
|
35
39
|
};
|
|
36
40
|
if (options.input !== undefined)
|
|
37
41
|
readOptions.input = options.input;
|
|
42
|
+
// IPC relay to the MCP-server process (Task 3.24). The MCP process's
|
|
43
|
+
// LocalSocketHostBridge connects here; the server relays `act.request` →
|
|
44
|
+
// native-port `action.request` (via `write`, the host → SW direction) and
|
|
45
|
+
// `action.result` (inbound, see handleMessage) → `act.response`.
|
|
46
|
+
let socketServer;
|
|
47
|
+
if (options.startSocketServer !== false) {
|
|
48
|
+
// Resolve the socket path: explicit option wins; else derive from the
|
|
49
|
+
// (possibly test-overridden) peek data dir via hostSocketPath(home). This
|
|
50
|
+
// is the SAME function the MCP-process bridge dials, so both sides agree —
|
|
51
|
+
// including under a custom $PEEK_HOME (item D). On Windows it's the fixed
|
|
52
|
+
// named pipe; a test `home` binds its own socket and never collides.
|
|
53
|
+
const socketPath = options.socketPath ?? hostSocketPath(home);
|
|
54
|
+
socketServer = new HostSocketServer({
|
|
55
|
+
// postToSw writes the action.request out the native port (host → SW).
|
|
56
|
+
postToSw: (message) => {
|
|
57
|
+
void write(message).catch((err) => {
|
|
58
|
+
console.error(`native host: action.request post failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
socketPath,
|
|
62
|
+
// Item I/J: bind failures (EADDRINUSE/EACCES) are emitted ASYNC via the
|
|
63
|
+
// server 'error' event, so a try/catch around listen() can't catch them.
|
|
64
|
+
// The server probes live-vs-stale + retries once on a stale socket; if it
|
|
65
|
+
// still can't bind it calls this. Degrade — the action write-path is down,
|
|
66
|
+
// but ingest/capture still flows — never crash the host.
|
|
67
|
+
onListenError: (err) => {
|
|
68
|
+
console.error(`native host: IPC socket failed to listen — ${err.message}`);
|
|
69
|
+
socketServer = undefined;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
// listen() is async-internally (the bind-error/probe/retry path); it never
|
|
73
|
+
// throws synchronously, but keep the try/catch for a defensive construct-time
|
|
74
|
+
// failure.
|
|
75
|
+
try {
|
|
76
|
+
socketServer.listen();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(`native host: IPC socket listen threw — ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
+
socketServer = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
38
83
|
const done = readMessages((message) => {
|
|
39
84
|
// A reply-write failure (e.g. the browser closed the port mid-reply) must
|
|
40
85
|
// not become an unhandled rejection that tears down the host.
|
|
@@ -45,6 +90,7 @@ export function startNativeHost(options = {}) {
|
|
|
45
90
|
return {
|
|
46
91
|
done,
|
|
47
92
|
close() {
|
|
93
|
+
socketServer?.close();
|
|
48
94
|
db.close();
|
|
49
95
|
},
|
|
50
96
|
};
|
|
@@ -60,6 +106,14 @@ export function startNativeHost(options = {}) {
|
|
|
60
106
|
});
|
|
61
107
|
return;
|
|
62
108
|
}
|
|
109
|
+
// Action-request correlation replies from the SW (Task 3.24). Forward to
|
|
110
|
+
// the IPC relay, which writes the mapped `act.response` back to the
|
|
111
|
+
// originating MCP-process socket connection. These are NOT acked over the
|
|
112
|
+
// native port (the MCP process is the one awaiting, over the socket).
|
|
113
|
+
if (type !== undefined && ACTION_REPLY_TYPES.has(type)) {
|
|
114
|
+
socketServer?.onSwMessage(message);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
63
117
|
if (type !== undefined && INGEST_TYPES.has(type)) {
|
|
64
118
|
// ingest() catches its own throws + returns a structured reply. The
|
|
65
119
|
// try/catch here is defense in depth: if a future regression introduces
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/native-host/host.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,wEAAwE;AACxE,8EAA8E;AAC9E,0DAA0D;AAC1D,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAC3E,sEAAsE;AACtE,yEAAyE;AACzE,8EAA8E;AAG9E,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/native-host/host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local socket / named-pipe path the {@link LocalSocketHostBridge} connects
|
|
3
|
+
* to and the {@link HostSocketServer} listens on.
|
|
4
|
+
*
|
|
5
|
+
* @param peekHome the peek data dir (defaults to {@link peekHomeDir}, which
|
|
6
|
+
* honors `$PEEK_HOME`). On win32 the named pipe is a fixed namespace path and
|
|
7
|
+
* this argument is ignored.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hostSocketPath(peekHome?: string): string;
|
|
10
|
+
//# sourceMappingURL=socket-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-path.d.ts","sourceRoot":"","sources":["../../src/native-host/socket-path.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAgB,GAAG,MAAM,CAI/D"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Local IPC endpoint path for the MCP-process ↔ native-host bridge (Task 3.24
|
|
2
|
+
// IPC layer). The two `peek-mcp` processes share the peek data dir; the
|
|
3
|
+
// simplest cross-platform local-only IPC is a Unix domain socket on
|
|
4
|
+
// macOS/Linux (`<peekHome>/host.sock`) and a named pipe on Windows.
|
|
5
|
+
//
|
|
6
|
+
// Item D: the socket lives DIRECTLY inside the peek data dir (peekHomeDir(),
|
|
7
|
+
// which honors $PEEK_HOME) as `host.sock`. The native host binds at the same
|
|
8
|
+
// `join(peekHomeDir(), 'host.sock')`, so a user who relocates the store via
|
|
9
|
+
// PEEK_HOME gets the bridge and the host on ONE path — not the bridge dialing
|
|
10
|
+
// ~/.peek/host.sock while the host listens elsewhere.
|
|
11
|
+
//
|
|
12
|
+
// Pure path derivation — no `node:net`, no filesystem — so it unit-tests
|
|
13
|
+
// cleanly. The peek-home dir is injectable for tests.
|
|
14
|
+
import { platform } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { peekHomeDir } from '../db/open.js';
|
|
17
|
+
/**
|
|
18
|
+
* The local socket / named-pipe path the {@link LocalSocketHostBridge} connects
|
|
19
|
+
* to and the {@link HostSocketServer} listens on.
|
|
20
|
+
*
|
|
21
|
+
* @param peekHome the peek data dir (defaults to {@link peekHomeDir}, which
|
|
22
|
+
* honors `$PEEK_HOME`). On win32 the named pipe is a fixed namespace path and
|
|
23
|
+
* this argument is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export function hostSocketPath(peekHome = peekHomeDir()) {
|
|
26
|
+
// Windows named pipes live in the `\\.\pipe\` namespace, not the filesystem.
|
|
27
|
+
if (platform() === 'win32')
|
|
28
|
+
return '\\\\.\\pipe\\peek-host';
|
|
29
|
+
return join(peekHome, 'host.sock');
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=socket-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-path.js","sourceRoot":"","sources":["../../src/native-host/socket-path.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAC9E,sDAAsD;AACtD,EAAE;AACF,yEAAyE;AACzE,sDAAsD;AAEtD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,QAAQ,GAAG,WAAW,EAAE;IACrD,6EAA6E;IAC7E,IAAI,QAAQ,EAAE,KAAK,OAAO;QAAE,OAAO,wBAAwB,CAAC;IAC5D,OAAO,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AACrC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peekdev/mcp",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14",
|
|
4
|
+
"mcpName": "io.github.Cubenest/peek-mcp",
|
|
4
5
|
"description": "peek native messaging host + stdio MCP server. Owns ~/.peek/sessions.db (better-sqlite3) and bridges the browser extension, CLI, and AI tools to a single local source of truth.",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"peek",
|