@hivemind-os/collective-daemon 0.2.0

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.
@@ -0,0 +1,1196 @@
1
+ import {
2
+ validateClientProcessOwnership,
3
+ verifyPipeSecurity
4
+ } from "./chunk-NXIFS427.js";
5
+
6
+ // src/ipc/server.ts
7
+ import { chmod, rm } from "fs/promises";
8
+ import net2 from "net";
9
+ import pino3 from "pino";
10
+
11
+ // src/ipc/connection.ts
12
+ import { randomUUID as randomUUID3 } from "crypto";
13
+ import "net";
14
+ import pino2 from "pino";
15
+
16
+ // src/audit.ts
17
+ import pino from "pino";
18
+ var logger = pino({ name: "@hivemind-os/collective-daemon:audit" });
19
+ var listeners = /* @__PURE__ */ new Set();
20
+ function logAuditEvent(event) {
21
+ logger.info(event);
22
+ for (const listener of listeners) {
23
+ listener(event);
24
+ }
25
+ }
26
+
27
+ // src/mcp/session.ts
28
+ import { randomUUID as randomUUID2 } from "crypto";
29
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
30
+ import {
31
+ CallToolRequestSchema,
32
+ GetTaskRequestSchema,
33
+ GetTaskPayloadRequestSchema,
34
+ ListTasksRequestSchema,
35
+ CancelTaskRequestSchema,
36
+ ListToolsRequestSchema
37
+ } from "@modelcontextprotocol/sdk/types.js";
38
+ import { SessionExpiredError } from "@hivemind-os/collective-core";
39
+ import {
40
+ meshToolDefinitions,
41
+ meshToolHandlers,
42
+ registerResourceHandlers
43
+ } from "@hivemind-os/collective-mcp-server";
44
+ import { PaymentRail, TaskStatus } from "@hivemind-os/collective-types";
45
+
46
+ // src/ipc/protocol.ts
47
+ function parseMessage(line) {
48
+ try {
49
+ const parsed = JSON.parse(line);
50
+ if (!parsed || parsed.jsonrpc !== "2.0") {
51
+ return null;
52
+ }
53
+ if (typeof parsed.method === "string") {
54
+ if ("id" in parsed) {
55
+ if (typeof parsed.id !== "string" && typeof parsed.id !== "number") {
56
+ return null;
57
+ }
58
+ return {
59
+ jsonrpc: "2.0",
60
+ id: parsed.id,
61
+ method: parsed.method,
62
+ params: parsed.params
63
+ };
64
+ }
65
+ return {
66
+ jsonrpc: "2.0",
67
+ method: parsed.method,
68
+ params: parsed.params
69
+ };
70
+ }
71
+ if ("id" in parsed && (typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null)) {
72
+ const response = {
73
+ jsonrpc: "2.0",
74
+ id: parsed.id
75
+ };
76
+ if ("result" in parsed) {
77
+ response.result = parsed.result;
78
+ }
79
+ if ("error" in parsed) {
80
+ const error = parsed.error;
81
+ if (!error || typeof error !== "object") {
82
+ return null;
83
+ }
84
+ const code = error.code;
85
+ const message = error.message;
86
+ if (typeof code !== "number" || typeof message !== "string") {
87
+ return null;
88
+ }
89
+ response.error = {
90
+ code,
91
+ message,
92
+ data: error.data
93
+ };
94
+ }
95
+ if (!("result" in parsed) && !("error" in parsed)) {
96
+ return null;
97
+ }
98
+ return response;
99
+ }
100
+ return null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ function serializeResponse(response) {
106
+ return JSON.stringify(response);
107
+ }
108
+ function createErrorResponse(id, code, message, data) {
109
+ return {
110
+ jsonrpc: "2.0",
111
+ id,
112
+ error: { code, message, data }
113
+ };
114
+ }
115
+ function isJsonRpcRequest(message) {
116
+ return "method" in message && "id" in message;
117
+ }
118
+ function isJsonRpcNotification(message) {
119
+ return "method" in message && !("id" in message);
120
+ }
121
+
122
+ // src/mcp/task-store.ts
123
+ import { randomUUID } from "crypto";
124
+ var DEFAULT_TTL_MS = 36e5;
125
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
126
+ var McpTaskStore = class {
127
+ tasks = /* @__PURE__ */ new Map();
128
+ chainToMcpId = /* @__PURE__ */ new Map();
129
+ cleanupTimers = /* @__PURE__ */ new Map();
130
+ create(onChainTaskId, options) {
131
+ const taskId = randomUUID();
132
+ const now = (/* @__PURE__ */ new Date()).toISOString();
133
+ const ttl = options?.ttl !== void 0 ? options.ttl : DEFAULT_TTL_MS;
134
+ const entry = {
135
+ taskId,
136
+ onChainTaskId,
137
+ status: "working",
138
+ createdAt: now,
139
+ lastUpdatedAt: now,
140
+ ttl,
141
+ pollInterval: options?.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
142
+ progressToken: options?.progressToken
143
+ };
144
+ this.tasks.set(taskId, entry);
145
+ this.chainToMcpId.set(onChainTaskId, taskId);
146
+ this.scheduleCleanup(entry);
147
+ return entry;
148
+ }
149
+ get(taskId) {
150
+ return this.tasks.get(taskId);
151
+ }
152
+ getByChainId(onChainTaskId) {
153
+ const mcpId = this.chainToMcpId.get(onChainTaskId);
154
+ return mcpId ? this.tasks.get(mcpId) : void 0;
155
+ }
156
+ update(taskId, status, options) {
157
+ const entry = this.tasks.get(taskId);
158
+ if (!entry) {
159
+ return void 0;
160
+ }
161
+ entry.status = status;
162
+ entry.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
163
+ if (options?.statusMessage !== void 0) {
164
+ entry.statusMessage = options.statusMessage;
165
+ }
166
+ if (options?.result !== void 0) {
167
+ entry.result = options.result;
168
+ }
169
+ if (status === "completed" || status === "failed" || status === "cancelled") {
170
+ this.scheduleCleanup(entry);
171
+ }
172
+ return entry;
173
+ }
174
+ cancel(taskId) {
175
+ return this.update(taskId, "cancelled", { statusMessage: "Cancelled by client" });
176
+ }
177
+ list() {
178
+ return [...this.tasks.values()];
179
+ }
180
+ cleanup() {
181
+ for (const timer of this.cleanupTimers.values()) {
182
+ clearTimeout(timer);
183
+ }
184
+ this.cleanupTimers.clear();
185
+ this.tasks.clear();
186
+ this.chainToMcpId.clear();
187
+ }
188
+ scheduleCleanup(entry) {
189
+ if (entry.ttl === null) {
190
+ return;
191
+ }
192
+ const existing = this.cleanupTimers.get(entry.taskId);
193
+ if (existing) {
194
+ clearTimeout(existing);
195
+ }
196
+ const timer = setTimeout(() => {
197
+ this.tasks.delete(entry.taskId);
198
+ this.chainToMcpId.delete(entry.onChainTaskId);
199
+ this.cleanupTimers.delete(entry.taskId);
200
+ }, entry.ttl);
201
+ if (typeof timer === "object" && "unref" in timer) {
202
+ timer.unref();
203
+ }
204
+ this.cleanupTimers.set(entry.taskId, timer);
205
+ }
206
+ };
207
+
208
+ // src/mcp/session.ts
209
+ var IpcTransport = class {
210
+ constructor(emit) {
211
+ this.emit = emit;
212
+ }
213
+ emit;
214
+ onclose;
215
+ onerror;
216
+ onmessage;
217
+ sessionId = randomUUID2();
218
+ closed = false;
219
+ pendingResponses = /* @__PURE__ */ new Map();
220
+ async start() {
221
+ return;
222
+ }
223
+ async send(message) {
224
+ const jsonRpcMessage = message;
225
+ if (isJsonRpcResponse(jsonRpcMessage) && jsonRpcMessage.id !== null) {
226
+ this.pendingResponses.get(jsonRpcMessage.id)?.(jsonRpcMessage);
227
+ this.pendingResponses.delete(jsonRpcMessage.id);
228
+ }
229
+ await this.emit(jsonRpcMessage);
230
+ }
231
+ async close() {
232
+ if (this.closed) {
233
+ return;
234
+ }
235
+ this.closed = true;
236
+ this.pendingResponses.clear();
237
+ this.onclose?.();
238
+ }
239
+ async dispatch(message) {
240
+ if (this.closed) {
241
+ throw new Error("Transport is closed.");
242
+ }
243
+ this.onmessage?.(message);
244
+ }
245
+ waitForResponse(id) {
246
+ return new Promise((resolvePromise) => {
247
+ this.pendingResponses.set(id, resolvePromise);
248
+ });
249
+ }
250
+ };
251
+ var McpSession = class {
252
+ server;
253
+ transport;
254
+ state;
255
+ getStatus;
256
+ getAppName;
257
+ toolContext;
258
+ taskStore = new McpTaskStore();
259
+ initializePromise;
260
+ constructor(params) {
261
+ this.state = params.state;
262
+ this.getStatus = params.getStatus;
263
+ this.getAppName = params.getAppName;
264
+ this.toolContext = params.toolContext;
265
+ this.server = new Server(
266
+ { name: "@hivemind-os/collective-daemon", version: "0.1.0" },
267
+ {
268
+ capabilities: {
269
+ tools: {},
270
+ tasks: {},
271
+ ...params.toolContext ? { resources: {} } : {}
272
+ }
273
+ }
274
+ );
275
+ this.transport = new IpcTransport(params.emit);
276
+ }
277
+ async initialize() {
278
+ if (this.initializePromise) {
279
+ await this.initializePromise;
280
+ return;
281
+ }
282
+ this.registerTools();
283
+ this.initializePromise = this.server.connect(this.transport);
284
+ await this.initializePromise;
285
+ }
286
+ async handleMessage(message) {
287
+ if (isJsonRpcRequest(message)) {
288
+ const responsePromise = this.transport.waitForResponse(message.id);
289
+ await this.transport.dispatch(message);
290
+ return responsePromise;
291
+ }
292
+ await this.transport.dispatch(message);
293
+ return null;
294
+ }
295
+ evaluateSpending(request) {
296
+ return this.state.spendingPolicy.evaluate({
297
+ ...request,
298
+ originAppName: this.getAppName()
299
+ });
300
+ }
301
+ recordSpending(entry) {
302
+ const appName = this.getAppName();
303
+ logAuditEvent({
304
+ event: "spending",
305
+ appName,
306
+ amount: entry.amountMist.toString(),
307
+ taskId: entry.taskId
308
+ });
309
+ this.state.spendingPolicy.record({
310
+ ...entry,
311
+ originAppName: appName
312
+ });
313
+ }
314
+ async close() {
315
+ this.taskStore.cleanup();
316
+ await this.server.close();
317
+ }
318
+ /** Expose the low-level MCP Server for sampling requests. */
319
+ get mcpServer() {
320
+ return this.server;
321
+ }
322
+ /** Get the per-session MCP task store. */
323
+ getTaskStore() {
324
+ return this.taskStore;
325
+ }
326
+ /** Send a progress notification to the connected client. */
327
+ async sendProgress(progressToken, progress, total, message) {
328
+ await this.server.notification({
329
+ method: "notifications/progress",
330
+ params: { progressToken, progress, total, message }
331
+ });
332
+ }
333
+ /** Send a task status notification to the connected client. */
334
+ async sendTaskStatusNotification(entry) {
335
+ await this.server.notification({
336
+ method: "notifications/tasks/status",
337
+ params: {
338
+ taskId: entry.taskId,
339
+ status: entry.status,
340
+ ttl: entry.ttl,
341
+ createdAt: entry.createdAt,
342
+ lastUpdatedAt: entry.lastUpdatedAt,
343
+ ...entry.pollInterval !== void 0 ? { pollInterval: entry.pollInterval } : {},
344
+ ...entry.statusMessage !== void 0 ? { statusMessage: entry.statusMessage } : {}
345
+ }
346
+ });
347
+ }
348
+ registerTools() {
349
+ const daemonToolDefs = [
350
+ {
351
+ name: "collective_balance",
352
+ description: "Return the daemon wallet SUI balance, address, and DID.",
353
+ inputSchema: { type: "object", properties: {} }
354
+ },
355
+ {
356
+ name: "collective_status",
357
+ description: "Return daemon identity, uptime, and connected apps.",
358
+ inputSchema: { type: "object", properties: {} }
359
+ }
360
+ ];
361
+ const daemonToolHandlers = {
362
+ collective_balance: async () => {
363
+ const balanceMist = await this.state.suiClient.getBalance(this.state.address);
364
+ return {
365
+ did: this.state.did,
366
+ address: this.state.address,
367
+ balanceMist: balanceMist.toString()
368
+ };
369
+ },
370
+ collective_status: async () => {
371
+ const status = this.getStatus();
372
+ return {
373
+ ...status,
374
+ connectedApps: status.connectedApps.map((app) => ({ ...app, pid: app.appPid }))
375
+ };
376
+ }
377
+ };
378
+ const allToolDefs = [
379
+ ...daemonToolDefs,
380
+ ...meshToolDefinitions.filter((def) => !daemonToolHandlers[def.name]).map((def) => {
381
+ if (def.name === "collective_execute") {
382
+ return { ...def, execution: { taskSupport: "optional" } };
383
+ }
384
+ return def;
385
+ })
386
+ ];
387
+ const context = this.toolContext;
388
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
389
+ tools: allToolDefs
390
+ }));
391
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
392
+ const toolName = request.params.name;
393
+ const meta = request.params._meta;
394
+ const progressToken = meta?.progressToken;
395
+ const daemonHandler = daemonToolHandlers[toolName];
396
+ if (daemonHandler) {
397
+ try {
398
+ const result = await daemonHandler();
399
+ return createDaemonToolResult(result);
400
+ } catch (error) {
401
+ return createErrorResult(error instanceof Error ? error.message : String(error));
402
+ }
403
+ }
404
+ const meshHandler = meshToolHandlers[toolName];
405
+ if (meshHandler && context) {
406
+ if (toolName === "collective_execute" && this.clientSupportsTasks()) {
407
+ try {
408
+ return await this.handleExecuteAsTask(request.params.arguments, context, progressToken);
409
+ } catch (error) {
410
+ const message = error instanceof Error ? error.message : String(error);
411
+ return createErrorResult(message);
412
+ }
413
+ }
414
+ try {
415
+ const callContext = { ...context, originAppName: this.getAppName() };
416
+ const result = await meshHandler(request.params.arguments ?? {}, callContext);
417
+ return createSuccessResult(result);
418
+ } catch (error) {
419
+ const message = error instanceof SessionExpiredError ? "Authentication expired. Please re-authenticate via the daemon portal." : error instanceof Error ? error.message : String(error);
420
+ return createErrorResult(message);
421
+ }
422
+ }
423
+ return createErrorResult(`Unknown tool: ${toolName}`);
424
+ });
425
+ this.registerTaskHandlers();
426
+ if (context) {
427
+ registerResourceHandlers(this.server, context);
428
+ }
429
+ }
430
+ /**
431
+ * Check whether the connected client advertises MCP Tasks support.
432
+ * Only VS Code Copilot (as of 2025-11-25 spec) does this; Claude Desktop,
433
+ * ChatGPT, Cursor, Windsurf, and GH Coding Agent do not.
434
+ */
435
+ clientSupportsTasks() {
436
+ const caps = this.server.getClientCapabilities();
437
+ return caps?.tasks != null;
438
+ }
439
+ /**
440
+ * Handle collective_execute as an MCP Task: post the on-chain task, return immediately
441
+ * with a task handle, and track completion in the background.
442
+ */
443
+ async handleExecuteAsTask(args, context, progressToken) {
444
+ const callContext = { ...context, originAppName: this.getAppName() };
445
+ const executeAsyncHandler = meshToolHandlers["collective_execute_async"];
446
+ if (!executeAsyncHandler) {
447
+ throw new Error("collective_execute_async handler not found");
448
+ }
449
+ const asyncResult = await executeAsyncHandler(args, callContext);
450
+ const mcpTask = this.taskStore.create(asyncResult.task_id, {
451
+ progressToken,
452
+ pollInterval: 2e3
453
+ });
454
+ void this.trackTaskCompletion(mcpTask.taskId, asyncResult.task_id, callContext, {
455
+ providerDid: asyncResult.provider_did,
456
+ priceMist: BigInt(asyncResult.price_mist)
457
+ });
458
+ return {
459
+ task: mcpTask
460
+ };
461
+ }
462
+ /**
463
+ * Background loop that polls on-chain task status and sends notifications.
464
+ */
465
+ async trackTaskCompletion(mcpTaskId, onChainTaskId, context, params) {
466
+ const POLL_INTERVAL_MS = 2e3;
467
+ const MAX_DURATION_MS = 3e5;
468
+ const startedAt = Date.now();
469
+ let lastChainStatus;
470
+ const entry = this.taskStore.get(mcpTaskId);
471
+ const progressToken = entry?.progressToken;
472
+ try {
473
+ while (true) {
474
+ if (Date.now() - startedAt > MAX_DURATION_MS) {
475
+ this.taskStore.update(mcpTaskId, "failed", { statusMessage: "Timed out waiting for provider" });
476
+ await this.sendTaskStatusNotification(this.taskStore.get(mcpTaskId));
477
+ return;
478
+ }
479
+ const task = await context.taskClient.getTask(onChainTaskId);
480
+ if (!task) {
481
+ this.taskStore.update(mcpTaskId, "failed", { statusMessage: "Task not found on chain" });
482
+ await this.sendTaskStatusNotification(this.taskStore.get(mcpTaskId));
483
+ return;
484
+ }
485
+ if (task.status !== lastChainStatus) {
486
+ lastChainStatus = task.status;
487
+ if (progressToken !== void 0) {
488
+ await this.emitProgressForChainStatus(progressToken, task.status);
489
+ }
490
+ }
491
+ const currentEntry = this.taskStore.get(mcpTaskId);
492
+ if (!currentEntry || currentEntry.status === "cancelled") {
493
+ return;
494
+ }
495
+ if (task.status === TaskStatus.COMPLETED || task.status === TaskStatus.RELEASED) {
496
+ let resultText = "";
497
+ if (task.resultBlobId) {
498
+ const resultBytes = await context.blobStore.fetch(task.resultBlobId);
499
+ if (resultBytes) {
500
+ resultText = new TextDecoder().decode(resultBytes);
501
+ }
502
+ }
503
+ if (task.status === TaskStatus.COMPLETED) {
504
+ await context.taskClient.releasePayment({
505
+ taskId: onChainTaskId,
506
+ keypair: context.keypair
507
+ });
508
+ context.spendingPolicy.record({
509
+ amountMist: params.priceMist,
510
+ rail: PaymentRail.SUI_ESCROW,
511
+ taskId: onChainTaskId,
512
+ appId: params.providerDid,
513
+ originAppName: context.originAppName
514
+ });
515
+ }
516
+ const toolResult = {
517
+ content: [{
518
+ type: "text",
519
+ text: serialize({
520
+ task_id: onChainTaskId,
521
+ result: resultText,
522
+ provider_did: params.providerDid,
523
+ price_mist: params.priceMist.toString(),
524
+ status: "RELEASED",
525
+ execution_mode: "async",
526
+ payment_rail: PaymentRail.SUI_ESCROW
527
+ })
528
+ }]
529
+ };
530
+ this.taskStore.update(mcpTaskId, "completed", { result: toolResult });
531
+ await this.sendTaskStatusNotification(this.taskStore.get(mcpTaskId));
532
+ return;
533
+ }
534
+ if (task.status === TaskStatus.CANCELLED || task.status === TaskStatus.DISPUTED) {
535
+ this.taskStore.update(mcpTaskId, "failed", {
536
+ statusMessage: `Task ended with status ${TaskStatus[task.status]}`
537
+ });
538
+ await this.sendTaskStatusNotification(this.taskStore.get(mcpTaskId));
539
+ return;
540
+ }
541
+ await delay(POLL_INTERVAL_MS);
542
+ }
543
+ } catch (error) {
544
+ const message = error instanceof Error ? error.message : String(error);
545
+ this.taskStore.update(mcpTaskId, "failed", { statusMessage: message });
546
+ try {
547
+ await this.sendTaskStatusNotification(this.taskStore.get(mcpTaskId));
548
+ } catch {
549
+ }
550
+ }
551
+ }
552
+ async emitProgressForChainStatus(progressToken, chainStatus) {
553
+ const stages = {
554
+ [TaskStatus.POSTED]: { progress: 0.25, message: "Escrow posted, waiting for provider" },
555
+ [TaskStatus.ACCEPTED]: { progress: 0.5, message: "Task accepted by provider" },
556
+ [TaskStatus.COMPLETED]: { progress: 0.9, message: "Provider completed, verifying result" },
557
+ [TaskStatus.RELEASED]: { progress: 1, message: "Payment released, task complete" }
558
+ };
559
+ const stage = stages[chainStatus];
560
+ if (stage) {
561
+ try {
562
+ await this.sendProgress(progressToken, stage.progress, 1, stage.message);
563
+ } catch {
564
+ }
565
+ }
566
+ }
567
+ registerTaskHandlers() {
568
+ this.server.setRequestHandler(GetTaskRequestSchema, async (request) => {
569
+ const entry = this.taskStore.get(request.params.taskId);
570
+ if (!entry) {
571
+ throw new Error(`Task ${request.params.taskId} not found`);
572
+ }
573
+ return toTaskResult(entry);
574
+ });
575
+ this.server.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
576
+ const entry = this.taskStore.get(request.params.taskId);
577
+ if (!entry) {
578
+ throw new Error(`Task ${request.params.taskId} not found`);
579
+ }
580
+ if (entry.status !== "completed" || !entry.result) {
581
+ throw new Error(`Task ${request.params.taskId} is not yet completed (status: ${entry.status})`);
582
+ }
583
+ return entry.result;
584
+ });
585
+ this.server.setRequestHandler(ListTasksRequestSchema, async () => {
586
+ const tasks = this.taskStore.list().map((entry) => toTaskResult(entry));
587
+ return { tasks };
588
+ });
589
+ this.server.setRequestHandler(CancelTaskRequestSchema, async (request) => {
590
+ const entry = this.taskStore.get(request.params.taskId);
591
+ if (!entry) {
592
+ throw new Error(`Task ${request.params.taskId} not found`);
593
+ }
594
+ if (entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled") {
595
+ return toTaskResult(entry);
596
+ }
597
+ if (this.toolContext) {
598
+ try {
599
+ const chainTask = await this.toolContext.taskClient.getTask(entry.onChainTaskId);
600
+ if (chainTask) {
601
+ if (chainTask.status === TaskStatus.POSTED) {
602
+ await this.toolContext.taskClient.cancelTask({
603
+ taskId: entry.onChainTaskId,
604
+ keypair: this.toolContext.keypair
605
+ });
606
+ } else if (chainTask.status === TaskStatus.ACCEPTED) {
607
+ await this.toolContext.taskClient.disputeTask({
608
+ taskId: entry.onChainTaskId,
609
+ keypair: this.toolContext.keypair
610
+ });
611
+ }
612
+ }
613
+ } catch {
614
+ }
615
+ }
616
+ const cancelled = this.taskStore.cancel(request.params.taskId);
617
+ return toTaskResult(cancelled ?? entry);
618
+ });
619
+ }
620
+ };
621
+ function createDaemonToolResult(payload) {
622
+ return {
623
+ content: [{ type: "text", text: serialize(payload) }],
624
+ structuredContent: payload
625
+ };
626
+ }
627
+ function createSuccessResult(payload) {
628
+ return {
629
+ content: [{ type: "text", text: serialize(payload) }]
630
+ };
631
+ }
632
+ function createErrorResult(message) {
633
+ return {
634
+ isError: true,
635
+ content: [{ type: "text", text: serialize({ error: message }) }]
636
+ };
637
+ }
638
+ function toTaskResult(entry) {
639
+ return {
640
+ taskId: entry.taskId,
641
+ status: entry.status,
642
+ ttl: entry.ttl,
643
+ createdAt: entry.createdAt,
644
+ lastUpdatedAt: entry.lastUpdatedAt,
645
+ ...entry.pollInterval !== void 0 ? { pollInterval: entry.pollInterval } : {},
646
+ ...entry.statusMessage !== void 0 ? { statusMessage: entry.statusMessage } : {}
647
+ };
648
+ }
649
+ function serialize(payload) {
650
+ return JSON.stringify(payload, bigintReplacer, 2);
651
+ }
652
+ function bigintReplacer(_key, value) {
653
+ return typeof value === "bigint" ? value.toString() : value;
654
+ }
655
+ function isJsonRpcResponse(message) {
656
+ return !("method" in message);
657
+ }
658
+ function delay(ms) {
659
+ return new Promise((resolvePromise) => {
660
+ setTimeout(resolvePromise, ms);
661
+ });
662
+ }
663
+
664
+ // src/ipc/connection.ts
665
+ var logger2 = pino2({ name: "@hivemind-os/collective-daemon:connection" });
666
+ var Connection = class {
667
+ constructor(socket, state, options) {
668
+ this.socket = socket;
669
+ this.state = state;
670
+ this.options = options;
671
+ this.socket.setEncoding("utf8");
672
+ this.socket.setNoDelay(true);
673
+ this.session = new McpSession({
674
+ state: this.state,
675
+ emit: async (message) => {
676
+ this.sendMessage(message);
677
+ },
678
+ getStatus: this.options.getStatus,
679
+ getAppName: () => this.appName ?? "unknown",
680
+ toolContext: this.options.toolContext
681
+ });
682
+ this.sessionReady = this.session.initialize();
683
+ this.socket.on("data", (chunk) => {
684
+ this.buffer += chunk.toString();
685
+ this.drainBuffer();
686
+ });
687
+ this.socket.on("close", () => {
688
+ this.close();
689
+ });
690
+ this.socket.on("end", () => {
691
+ this.close();
692
+ });
693
+ this.socket.on("error", (error) => {
694
+ logger2.debug({ err: error, connectionId: this.id }, "Socket error.");
695
+ this.close();
696
+ });
697
+ }
698
+ socket;
699
+ state;
700
+ options;
701
+ id = randomUUID3();
702
+ connectedAt = Date.now();
703
+ /** The app name set during shim_hello handshake, or undefined if not yet received. */
704
+ get connectedAppName() {
705
+ return this.appName;
706
+ }
707
+ /** The low-level MCP Server for this connection (for sampling requests). */
708
+ get mcpServer() {
709
+ return this.session.mcpServer;
710
+ }
711
+ appName;
712
+ appPid;
713
+ profile;
714
+ session;
715
+ sessionReady;
716
+ buffer = "";
717
+ helloReceived = false;
718
+ closed = false;
719
+ drainBuffer() {
720
+ let newlineIndex = this.buffer.indexOf("\n");
721
+ while (newlineIndex >= 0) {
722
+ const line = this.buffer.slice(0, newlineIndex).trim();
723
+ this.buffer = this.buffer.slice(newlineIndex + 1);
724
+ if (line) {
725
+ const message = parseMessage(line);
726
+ if (!message) {
727
+ this.sendMessage(createErrorResponse(null, -32700, "Parse error"));
728
+ } else {
729
+ void this.handleMessage(message).catch((error) => {
730
+ logger2.error({ err: error, connectionId: this.id }, "Failed to handle IPC message.");
731
+ if (isJsonRpcRequest(message)) {
732
+ this.sendMessage(createErrorResponse(message.id, -32603, "Internal error"));
733
+ }
734
+ });
735
+ }
736
+ }
737
+ newlineIndex = this.buffer.indexOf("\n");
738
+ }
739
+ }
740
+ async handleMessage(message) {
741
+ if (isJsonRpcRequest(message) && message.method === "shim_hello") {
742
+ if (this.helloReceived) {
743
+ this.sendMessage(createErrorResponse(message.id, -32600, "shim_hello has already been received"));
744
+ return;
745
+ }
746
+ await this.handleShimHello(message.id, message.params);
747
+ return;
748
+ }
749
+ if (!this.helloReceived) {
750
+ if (isJsonRpcRequest(message)) {
751
+ this.sendMessage(createErrorResponse(message.id, -32e3, "Expected shim_hello as the first message"));
752
+ }
753
+ return;
754
+ }
755
+ if (isJsonRpcRequest(message) && message.method === "daemon_status") {
756
+ this.handleDaemonStatus(message.id);
757
+ return;
758
+ }
759
+ if (isJsonRpcRequest(message) && message.method === "auth.status") {
760
+ this.handleAuthStatus(message.id);
761
+ return;
762
+ }
763
+ if (isJsonRpcRequest(message) && message.method === "auth.reauth") {
764
+ await this.handleAuthReauth(message.id);
765
+ return;
766
+ }
767
+ await this.handleMcpMessage(message);
768
+ }
769
+ async handleShimHello(id, params) {
770
+ if (!isRecord(params)) {
771
+ this.sendMessage(createErrorResponse(id, -32602, "shim_hello requires app metadata"));
772
+ return;
773
+ }
774
+ const appPid = readPid(params.pid ?? params.appPid);
775
+ const appName = readString(params.appName) ?? readString(params.clientName) ?? inferAppName(appPid);
776
+ const profile = readString(params.profile);
777
+ if (!appName || appPid === void 0) {
778
+ this.sendMessage(createErrorResponse(id, -32602, "shim_hello requires appName and pid"));
779
+ return;
780
+ }
781
+ const validation = await this.options.validateClient({
782
+ appName,
783
+ appPid,
784
+ profile
785
+ });
786
+ if (!validation.allowed) {
787
+ logger2.warn(
788
+ {
789
+ connectionId: this.id,
790
+ appName,
791
+ appPid,
792
+ expectedUser: validation.expectedUser,
793
+ actualUser: validation.actualUser,
794
+ reason: validation.reason
795
+ },
796
+ "IPC client validation failed."
797
+ );
798
+ this.rejectAndClose(id, validation.reason ?? "IPC client validation failed.");
799
+ return;
800
+ }
801
+ this.appName = appName;
802
+ this.appPid = appPid;
803
+ this.profile = profile;
804
+ this.helloReceived = true;
805
+ this.options.onHello({
806
+ appName: this.appName,
807
+ appPid: this.appPid,
808
+ profile: this.profile
809
+ });
810
+ logAuditEvent({
811
+ event: "app_connected",
812
+ appName: this.appName,
813
+ appPid: this.appPid,
814
+ connectionId: this.id
815
+ });
816
+ this.sendMessage({
817
+ jsonrpc: "2.0",
818
+ id,
819
+ result: {
820
+ acknowledged: true,
821
+ connectionId: this.id
822
+ }
823
+ });
824
+ }
825
+ handleDaemonStatus(id) {
826
+ const status = this.options.getStatus();
827
+ this.sendMessage({
828
+ jsonrpc: "2.0",
829
+ id,
830
+ result: {
831
+ did: status.did,
832
+ uptime: status.uptime,
833
+ connectedApps: status.connectedApps.map((app) => ({
834
+ appName: app.appName,
835
+ connectedAt: app.connectedAt
836
+ })),
837
+ spendingToday: status.spendingToday,
838
+ providerRunning: status.providerRunning
839
+ }
840
+ });
841
+ }
842
+ handleAuthStatus(id) {
843
+ this.sendMessage({
844
+ jsonrpc: "2.0",
845
+ id,
846
+ result: this.options.getAuthStatus()
847
+ });
848
+ }
849
+ async handleAuthReauth(id) {
850
+ const result = await this.options.triggerReauth();
851
+ this.sendMessage({
852
+ jsonrpc: "2.0",
853
+ id,
854
+ result
855
+ });
856
+ }
857
+ async handleMcpMessage(message) {
858
+ await this.sessionReady;
859
+ try {
860
+ if (isJsonRpcRequest(message)) {
861
+ const toolCall = parseToolCall(message);
862
+ if (toolCall) {
863
+ logAuditEvent({
864
+ event: "tool_call",
865
+ appName: this.appName ?? "unknown",
866
+ tool: toolCall.tool,
867
+ taskId: toolCall.taskId
868
+ });
869
+ }
870
+ }
871
+ await this.session.handleMessage(message);
872
+ } catch (error) {
873
+ logger2.error({ err: error, connectionId: this.id }, "MCP session error.");
874
+ if (isJsonRpcRequest(message)) {
875
+ this.sendMessage(createErrorResponse(message.id, -32603, "MCP session error"));
876
+ } else if (isJsonRpcNotification(message)) {
877
+ this.close();
878
+ }
879
+ }
880
+ }
881
+ sendNotification(method, params) {
882
+ this.sendMessage({
883
+ jsonrpc: "2.0",
884
+ method,
885
+ params
886
+ });
887
+ }
888
+ close() {
889
+ if (this.closed) {
890
+ return;
891
+ }
892
+ this.closed = true;
893
+ this.options.onClose();
894
+ if (this.appName) {
895
+ logAuditEvent({
896
+ event: "app_disconnected",
897
+ appName: this.appName,
898
+ connectionId: this.id,
899
+ duration: Date.now() - this.connectedAt
900
+ });
901
+ }
902
+ void this.session.close().catch((error) => {
903
+ logger2.debug({ err: error, connectionId: this.id }, "Failed to close MCP session cleanly.");
904
+ });
905
+ if (!this.socket.destroyed && !this.socket.writableEnded) {
906
+ this.socket.destroy();
907
+ }
908
+ }
909
+ rejectAndClose(id, message) {
910
+ if (this.closed || this.socket.destroyed) {
911
+ return;
912
+ }
913
+ this.socket.end(`${serializeResponse(createErrorResponse(id, -32001, message))}
914
+ `);
915
+ this.close();
916
+ }
917
+ sendMessage(message) {
918
+ if (this.closed || this.socket.destroyed) {
919
+ return;
920
+ }
921
+ this.socket.write(`${serializeResponse(message)}
922
+ `);
923
+ }
924
+ };
925
+ function parseToolCall(message) {
926
+ if (message.method !== "tools/call" || !isRecord(message.params) || typeof message.params.name !== "string") {
927
+ return null;
928
+ }
929
+ return {
930
+ tool: message.params.name,
931
+ taskId: readTaskId(message.params.arguments)
932
+ };
933
+ }
934
+ function readTaskId(value) {
935
+ if (!isRecord(value)) {
936
+ return void 0;
937
+ }
938
+ return readString(value.taskId) ?? readString(value.task_id);
939
+ }
940
+ function readPid(value) {
941
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
942
+ }
943
+ function inferAppName(pid) {
944
+ return typeof pid === "number" ? `app-${pid}` : void 0;
945
+ }
946
+ function readString(value) {
947
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
948
+ }
949
+ function isRecord(value) {
950
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
951
+ }
952
+
953
+ // src/ipc/connection-registry.ts
954
+ var ConnectionRegistry = class {
955
+ connections = /* @__PURE__ */ new Map();
956
+ registerConnection(connectionId, connectedAt = Date.now()) {
957
+ this.connections.set(connectionId, {
958
+ connectionId,
959
+ connectedAt
960
+ });
961
+ }
962
+ updateConnection(connectionId, metadata) {
963
+ const current = this.connections.get(connectionId);
964
+ const next = {
965
+ connectionId,
966
+ connectedAt: current?.connectedAt ?? Date.now(),
967
+ appName: metadata.appName,
968
+ appPid: metadata.appPid,
969
+ profile: metadata.profile
970
+ };
971
+ this.connections.set(connectionId, next);
972
+ return toConnectedApp(next);
973
+ }
974
+ unregisterConnection(connectionId) {
975
+ this.connections.delete(connectionId);
976
+ }
977
+ getConnectedApps() {
978
+ return Array.from(this.connections.values()).filter(isConnectedApp).map(toConnectedApp).sort((left, right) => left.connectedAt - right.connectedAt);
979
+ }
980
+ };
981
+ function isConnectedApp(entry) {
982
+ return typeof entry.appName === "string" && typeof entry.appPid === "number";
983
+ }
984
+ function toConnectedApp(entry) {
985
+ if (!isConnectedApp(entry)) {
986
+ throw new Error(`Connection ${entry.connectionId} is missing app metadata.`);
987
+ }
988
+ return {
989
+ connectionId: entry.connectionId,
990
+ connectedAt: entry.connectedAt,
991
+ appName: entry.appName,
992
+ appPid: entry.appPid,
993
+ profile: entry.profile
994
+ };
995
+ }
996
+
997
+ // src/ipc/server.ts
998
+ var logger3 = pino3({ name: "@hivemind-os/collective-daemon:ipc-server" });
999
+ var IpcServer = class {
1000
+ constructor(ipcPath, state, options = {}) {
1001
+ this.ipcPath = ipcPath;
1002
+ this.state = state;
1003
+ this.options = options;
1004
+ }
1005
+ ipcPath;
1006
+ state;
1007
+ options;
1008
+ server;
1009
+ connections = /* @__PURE__ */ new Map();
1010
+ connectionRegistry = new ConnectionRegistry();
1011
+ toolContext;
1012
+ async start() {
1013
+ if (this.server) {
1014
+ return;
1015
+ }
1016
+ if (process.platform !== "win32") {
1017
+ await rm(this.ipcPath, { force: true });
1018
+ }
1019
+ const server = net2.createServer((socket) => {
1020
+ this.handleConnection(socket);
1021
+ });
1022
+ await new Promise((resolvePromise, reject) => {
1023
+ server.once("error", reject);
1024
+ if (process.platform === "win32") {
1025
+ server.listen(this.ipcPath, () => {
1026
+ server.off("error", reject);
1027
+ resolvePromise();
1028
+ });
1029
+ return;
1030
+ }
1031
+ server.listen({ path: this.ipcPath, readableAll: false, writableAll: false }, () => {
1032
+ server.off("error", reject);
1033
+ resolvePromise();
1034
+ });
1035
+ });
1036
+ if (process.platform !== "win32") {
1037
+ await chmod(this.ipcPath, 384);
1038
+ logger3.debug({ ipcPath: this.ipcPath }, "IPC socket permissions set to 0600 for local-user isolation.");
1039
+ } else {
1040
+ await this.logPipeSecurity();
1041
+ }
1042
+ server.on("error", (error) => {
1043
+ logger3.error({ err: error }, "IPC server error.");
1044
+ });
1045
+ this.server = server;
1046
+ }
1047
+ async stop() {
1048
+ const server = this.server;
1049
+ if (!server) {
1050
+ return;
1051
+ }
1052
+ this.server = void 0;
1053
+ for (const connection of [...this.connections.values()]) {
1054
+ connection.close();
1055
+ }
1056
+ await new Promise((resolvePromise, reject) => {
1057
+ server.close((error) => {
1058
+ if (error) {
1059
+ reject(error);
1060
+ return;
1061
+ }
1062
+ resolvePromise();
1063
+ });
1064
+ });
1065
+ if (process.platform !== "win32") {
1066
+ await rm(this.ipcPath, { force: true });
1067
+ }
1068
+ }
1069
+ getConnectedApps() {
1070
+ return this.connectionRegistry.getConnectedApps();
1071
+ }
1072
+ getStatus() {
1073
+ return {
1074
+ ...this.state.getStatusBase(),
1075
+ connectedApps: this.getConnectedApps()
1076
+ };
1077
+ }
1078
+ getAuthStatus() {
1079
+ return this.options.getAuthStatus?.() ?? {
1080
+ authMode: this.state.authProvider.mode,
1081
+ authenticated: this.state.authProvider.isAuthenticated(),
1082
+ state: this.state.authProvider.isAuthenticated() ? "authenticated" : "reauth_required",
1083
+ address: this.state.authProvider.isAuthenticated() ? this.state.address : null,
1084
+ expiresAt: null,
1085
+ expiresInMs: null,
1086
+ refreshAvailable: false,
1087
+ lastError: null,
1088
+ updatedAt: Date.now()
1089
+ };
1090
+ }
1091
+ notifyAuthStatusChanged(status = this.getAuthStatus()) {
1092
+ for (const connection of this.connections.values()) {
1093
+ connection.sendNotification("auth.status_changed", status);
1094
+ }
1095
+ }
1096
+ /**
1097
+ * Broadcast a notification to all connected MCP sessions.
1098
+ * Used for provider inbound task notifications and other system-wide events.
1099
+ */
1100
+ broadcastNotification(method, params) {
1101
+ for (const connection of this.connections.values()) {
1102
+ connection.sendNotification(method, params);
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Look up the low-level MCP Server for a connected app by name.
1107
+ * Throws if multiple connections match (ambiguous).
1108
+ * Returns undefined if no match is found.
1109
+ */
1110
+ getMcpServerForApp(appName) {
1111
+ const matches = [];
1112
+ for (const connection of this.connections.values()) {
1113
+ if (connection.connectedAppName === appName) {
1114
+ matches.push(connection);
1115
+ }
1116
+ }
1117
+ if (matches.length > 1) {
1118
+ throw new Error(
1119
+ `Ambiguous MCP sampling target: ${matches.length} connections match appName "${appName}". Use a more specific identifier or disconnect duplicate clients.`
1120
+ );
1121
+ }
1122
+ return matches[0]?.mcpServer;
1123
+ }
1124
+ handleConnection(socket) {
1125
+ logger3.info({ ipcPath: this.ipcPath }, "Received IPC connection attempt.");
1126
+ const connection = new Connection(socket, this.state, {
1127
+ getStatus: () => this.getStatus(),
1128
+ getAuthStatus: () => this.getAuthStatus(),
1129
+ triggerReauth: () => this.options.triggerReauth?.() ?? Promise.resolve({ portalUrl: null, browserOpened: false, status: this.getAuthStatus() }),
1130
+ validateClient: (metadata) => this.validateClient(metadata),
1131
+ toolContext: this.toolContext,
1132
+ onHello: (metadata) => {
1133
+ this.connectionRegistry.updateConnection(connection.id, metadata);
1134
+ },
1135
+ onClose: () => {
1136
+ this.connections.delete(connection.id);
1137
+ this.connectionRegistry.unregisterConnection(connection.id);
1138
+ }
1139
+ });
1140
+ this.connections.set(connection.id, connection);
1141
+ this.connectionRegistry.registerConnection(connection.id, connection.connectedAt);
1142
+ }
1143
+ async logPipeSecurity() {
1144
+ try {
1145
+ const inspectPipeSecurity = this.options.verifyPipeSecurity ?? verifyPipeSecurity;
1146
+ const status = await inspectPipeSecurity(this.ipcPath);
1147
+ const bindings = {
1148
+ ipcPath: this.ipcPath,
1149
+ userScoped: status.userScoped,
1150
+ aclVerified: status.aclVerified,
1151
+ owner: status.acl?.owner,
1152
+ identities: status.acl?.identities
1153
+ };
1154
+ if (!status.userScoped) {
1155
+ logger3.warn(bindings, status.note);
1156
+ return;
1157
+ }
1158
+ if (!status.aclVerified) {
1159
+ logger3.debug(bindings, status.note);
1160
+ return;
1161
+ }
1162
+ logger3.info(bindings, status.note);
1163
+ } catch (error) {
1164
+ logger3.warn({ err: error, ipcPath: this.ipcPath }, "Failed to inspect Windows pipe security.");
1165
+ }
1166
+ }
1167
+ async validateClient(metadata) {
1168
+ if (process.platform !== "win32") {
1169
+ return {
1170
+ allowed: true,
1171
+ source: "unix-socket"
1172
+ };
1173
+ }
1174
+ const validateClientProcess = this.options.validateClient ?? ((client) => validateClientProcessOwnership(client.appPid));
1175
+ const validation = await validateClientProcess(metadata);
1176
+ if (!validation.allowed) {
1177
+ logger3.warn(
1178
+ {
1179
+ ipcPath: this.ipcPath,
1180
+ appName: metadata.appName,
1181
+ appPid: metadata.appPid,
1182
+ expectedUser: validation.expectedUser,
1183
+ actualUser: validation.actualUser,
1184
+ reason: validation.reason
1185
+ },
1186
+ "Rejected IPC client during Windows identity validation."
1187
+ );
1188
+ }
1189
+ return validation;
1190
+ }
1191
+ };
1192
+
1193
+ export {
1194
+ IpcServer
1195
+ };
1196
+ //# sourceMappingURL=chunk-BHN4GOYD.js.map