@akshar5/cohall 0.4.8 → 0.4.9
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/CHANGELOG.md +7 -0
- package/README.md +5 -3
- package/bin/cohall.js +368 -118
- package/bin/cohall.js.map +13 -13
- package/docs/install.md +4 -1
- package/docs/services.md +6 -3
- package/package.json +3 -3
package/bin/cohall.js
CHANGED
|
@@ -17,10 +17,11 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
|
|
18
18
|
// packages/protocol/src/index.ts
|
|
19
19
|
import { Schema } from "effect";
|
|
20
|
-
var version = "0.4.
|
|
20
|
+
var version = "0.4.9", maxSocketPayloadBytes, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
|
|
21
21
|
expected: "an ISO-8601 UTC timestamp"
|
|
22
22
|
})).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, TaskTraceEvent, TaskTrace, ThreadContext, CreateTaskInput, SocketEvent, ErrorResponse, terminalTaskStatuses, isTerminalTask = (task) => terminalTaskStatuses.has(task.status), now = () => Timestamp.make(new Date().toISOString()), makeDeviceId = () => DeviceId.make(crypto.randomUUID()), makeThreadId = () => ThreadId.make(crypto.randomUUID()), makeMessageId = () => MessageId.make(crypto.randomUUID()), makeTaskId = () => TaskId.make(crypto.randomUUID()), makeAuthSessionId = () => AuthSessionId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
|
|
23
23
|
var init_src = __esm(() => {
|
|
24
|
+
maxSocketPayloadBytes = 1024 * 1024;
|
|
24
25
|
DeviceId = uuid("DeviceId");
|
|
25
26
|
ThreadId = uuid("ThreadId");
|
|
26
27
|
MessageId = uuid("MessageId");
|
|
@@ -202,7 +203,7 @@ var init_src = __esm(() => {
|
|
|
202
203
|
|
|
203
204
|
// apps/relay/src/config.ts
|
|
204
205
|
import { Effect as Effect8, Schema as Schema9 } from "effect";
|
|
205
|
-
import { access as
|
|
206
|
+
import { access as access4, chmod as chmod4, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
|
|
206
207
|
import { homedir as homedir4, platform as platform4 } from "node:os";
|
|
207
208
|
import { join as join5, resolve as resolve2 } from "node:path";
|
|
208
209
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
@@ -214,7 +215,7 @@ var RelayConfiguration, defaultDataDirectory = () => {
|
|
|
214
215
|
}
|
|
215
216
|
return join5(process.env.XDG_DATA_HOME ?? join5(homedir4(), ".local", "share"), "cohall");
|
|
216
217
|
}, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
|
|
217
|
-
if (await
|
|
218
|
+
if (await access4(path).then(() => true).catch(() => false)) {
|
|
218
219
|
return (await readFile4(path, "utf8")).trim();
|
|
219
220
|
}
|
|
220
221
|
const token = generatedToken();
|
|
@@ -238,10 +239,11 @@ var init_config = __esm(() => {
|
|
|
238
239
|
RelayConfiguration = Schema9.Struct({
|
|
239
240
|
host: Schema9.NonEmptyString,
|
|
240
241
|
port: Schema9.Int.check(Schema9.isBetween({ minimum: 1, maximum: 65535 })),
|
|
241
|
-
token: Schema9.
|
|
242
|
+
token: Schema9.String.check(Schema9.isMinLength(32)),
|
|
242
243
|
dataDirectory: Schema9.NonEmptyString,
|
|
243
244
|
databasePath: Schema9.NonEmptyString,
|
|
244
|
-
allowRemote: Schema9.Boolean
|
|
245
|
+
allowRemote: Schema9.Boolean,
|
|
246
|
+
historyTaskLimit: Schema9.Int.check(Schema9.isBetween({ minimum: 100, maximum: 1e5 }))
|
|
245
247
|
});
|
|
246
248
|
loadEnvironmentConfiguration = Effect8.tryPromise({
|
|
247
249
|
try: async () => {
|
|
@@ -257,13 +259,18 @@ var init_config = __esm(() => {
|
|
|
257
259
|
await chmod4(dataDirectory, 448);
|
|
258
260
|
}
|
|
259
261
|
const token = process.env.COHALL_TOKEN ?? await tokenFrom(join5(dataDirectory, "owner-token"));
|
|
262
|
+
if (token.length < 32) {
|
|
263
|
+
throw new Error("The Cohall relay owner token must be at least 32 characters");
|
|
264
|
+
}
|
|
265
|
+
const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
|
|
260
266
|
return Schema9.decodeUnknownSync(RelayConfiguration)({
|
|
261
267
|
host,
|
|
262
268
|
port,
|
|
263
269
|
token,
|
|
264
270
|
dataDirectory,
|
|
265
271
|
databasePath: join5(dataDirectory, "cohall.db"),
|
|
266
|
-
allowRemote
|
|
272
|
+
allowRemote,
|
|
273
|
+
historyTaskLimit
|
|
267
274
|
});
|
|
268
275
|
},
|
|
269
276
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
@@ -277,12 +284,27 @@ class Hub {
|
|
|
277
284
|
#principals = new Map;
|
|
278
285
|
#devices = new Map;
|
|
279
286
|
#deviceIds = new Map;
|
|
287
|
+
#sessions = new Map;
|
|
288
|
+
#ownerSockets = new Set;
|
|
280
289
|
attach(socket, principal) {
|
|
290
|
+
if (principal.sessionId === undefined) {
|
|
291
|
+
if (this.#ownerSockets.size >= 16) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
this.#ownerSockets.add(socket);
|
|
295
|
+
} else {
|
|
296
|
+
const existing = this.#sessions.get(principal.sessionId);
|
|
297
|
+
if (existing !== undefined && existing.readyState === WebSocket2.OPEN) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
this.#sessions.set(principal.sessionId, socket);
|
|
301
|
+
}
|
|
281
302
|
this.#principals.set(socket, principal);
|
|
282
|
-
if (socket.data.
|
|
283
|
-
clearTimeout(socket.data.
|
|
284
|
-
socket.data.
|
|
303
|
+
if (socket.data.stageDeadline !== undefined) {
|
|
304
|
+
clearTimeout(socket.data.stageDeadline);
|
|
305
|
+
socket.data.stageDeadline = undefined;
|
|
285
306
|
}
|
|
307
|
+
return true;
|
|
286
308
|
}
|
|
287
309
|
isAuthorized(socket) {
|
|
288
310
|
const principal = this.#principals.get(socket);
|
|
@@ -299,19 +321,34 @@ class Hub {
|
|
|
299
321
|
return this.#principals.get(socket)?.boundDeviceId;
|
|
300
322
|
}
|
|
301
323
|
registerDevice(deviceId, socket) {
|
|
324
|
+
if (this.#deviceIds.has(socket)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
302
327
|
const previous = this.#devices.get(deviceId);
|
|
303
328
|
if (previous !== undefined && previous !== socket && previous.readyState === WebSocket2.OPEN) {
|
|
304
329
|
return false;
|
|
305
330
|
}
|
|
306
331
|
this.#devices.set(deviceId, socket);
|
|
307
332
|
this.#deviceIds.set(socket, deviceId);
|
|
333
|
+
if (socket.data.stageDeadline !== undefined) {
|
|
334
|
+
clearTimeout(socket.data.stageDeadline);
|
|
335
|
+
socket.data.stageDeadline = undefined;
|
|
336
|
+
}
|
|
308
337
|
return true;
|
|
309
338
|
}
|
|
339
|
+
pendingConnections() {
|
|
340
|
+
return this.#principals.size - this.#deviceIds.size;
|
|
341
|
+
}
|
|
310
342
|
detach(socket) {
|
|
311
|
-
if (socket.data.
|
|
312
|
-
clearTimeout(socket.data.
|
|
343
|
+
if (socket.data.stageDeadline !== undefined) {
|
|
344
|
+
clearTimeout(socket.data.stageDeadline);
|
|
313
345
|
}
|
|
346
|
+
const principal = this.#principals.get(socket);
|
|
314
347
|
this.#principals.delete(socket);
|
|
348
|
+
this.#ownerSockets.delete(socket);
|
|
349
|
+
if (principal?.sessionId !== undefined && this.#sessions.get(principal.sessionId) === socket) {
|
|
350
|
+
this.#sessions.delete(principal.sessionId);
|
|
351
|
+
}
|
|
315
352
|
const deviceId = this.#deviceIds.get(socket);
|
|
316
353
|
this.#deviceIds.delete(socket);
|
|
317
354
|
if (deviceId === undefined || this.#devices.get(deviceId) !== socket) {
|
|
@@ -527,7 +564,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
527
564
|
events.push(TaskTraceEvent.make({ kind: task.status, at: task.completedAt, detail }));
|
|
528
565
|
}
|
|
529
566
|
return events;
|
|
530
|
-
}, makeService = (db) => {
|
|
567
|
+
}, makeService = (db, retainedTerminalTasks = 1000) => {
|
|
531
568
|
const recordTaskTraceEvent = (taskId, kind, detail, createdAt = now()) => {
|
|
532
569
|
db.query(`INSERT INTO task_trace_events (task_id, kind, created_at, detail)
|
|
533
570
|
VALUES (?, ?, ?, ?)`).run(taskId, kind, createdAt, detail);
|
|
@@ -535,6 +572,21 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
535
572
|
SELECT id FROM task_trace_events WHERE task_id = ? ORDER BY id DESC LIMIT 1000
|
|
536
573
|
)`).run(taskId, taskId);
|
|
537
574
|
};
|
|
575
|
+
const pruneTerminalHistory = (preserveTaskId) => {
|
|
576
|
+
const exclusion = preserveTaskId === undefined ? "" : "AND id <> ?";
|
|
577
|
+
const offset = preserveTaskId === undefined ? retainedTerminalTasks : retainedTerminalTasks - 1;
|
|
578
|
+
const staleTasks = `SELECT id FROM tasks
|
|
579
|
+
WHERE status IN ('completed', 'failed', 'cancelled')
|
|
580
|
+
${exclusion}
|
|
581
|
+
ORDER BY completed_at DESC, updated_at DESC, id DESC
|
|
582
|
+
LIMIT -1 OFFSET ?`;
|
|
583
|
+
const parameters = preserveTaskId === undefined ? [offset] : [preserveTaskId, offset];
|
|
584
|
+
db.query(`DELETE FROM messages WHERE task_id IN (${staleTasks})`).run(...parameters);
|
|
585
|
+
db.query(`DELETE FROM tasks WHERE id IN (${staleTasks})`).run(...parameters);
|
|
586
|
+
db.query(`DELETE FROM threads
|
|
587
|
+
WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE tasks.thread_id = threads.id)
|
|
588
|
+
AND NOT EXISTS (SELECT 1 FROM messages WHERE messages.thread_id = threads.id)`).run();
|
|
589
|
+
};
|
|
538
590
|
const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
539
591
|
const getTask = Effect9.fn("RelayStore.getTask")(function* (taskId) {
|
|
540
592
|
const row = yield* Effect9.try({
|
|
@@ -632,6 +684,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
632
684
|
}
|
|
633
685
|
db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
|
|
634
686
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
|
|
687
|
+
pruneTerminalHistory();
|
|
635
688
|
})(),
|
|
636
689
|
catch: operationError("RelayStore.recover")
|
|
637
690
|
});
|
|
@@ -797,6 +850,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
797
850
|
if (result.changes === 1 && event !== undefined) {
|
|
798
851
|
recordTaskTraceEvent(taskId, event.kind, event.detail, timestamp);
|
|
799
852
|
}
|
|
853
|
+
if (result.changes === 1 && update.status !== undefined && ["completed", "failed", "cancelled"].includes(update.status)) {
|
|
854
|
+
pruneTerminalHistory(taskId);
|
|
855
|
+
}
|
|
800
856
|
})(),
|
|
801
857
|
catch: operationError("RelayStore.transition")
|
|
802
858
|
});
|
|
@@ -864,6 +920,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
864
920
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(makeMessageId(), current.threadId, role, status === "failed" ? "error" : "chat", deviceId, status === "completed" ? providerName(current.provider) : "Cohall", content, timestamp, taskId, deviceId);
|
|
865
921
|
db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, current.threadId);
|
|
866
922
|
}
|
|
923
|
+
pruneTerminalHistory(taskId);
|
|
867
924
|
})(),
|
|
868
925
|
catch: operationError("RelayStore.terminal")
|
|
869
926
|
});
|
|
@@ -1100,10 +1157,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1100
1157
|
}
|
|
1101
1158
|
},
|
|
1102
1159
|
catch: operationError("RelayStore.migrate")
|
|
1103
|
-
}), layer2 = (path) => Layer2.effect(Service2, Effect9.acquireRelease(Effect9.try({
|
|
1160
|
+
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect9.acquireRelease(Effect9.try({
|
|
1104
1161
|
try: () => new Database(path),
|
|
1105
1162
|
catch: operationError("RelayStore.open")
|
|
1106
|
-
}).pipe(Effect9.tap(migrate)), (db) => Effect9.sync(() => db.close())).pipe(Effect9.map(makeService))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect9.map(() => makeService(db))));
|
|
1163
|
+
}).pipe(Effect9.tap(migrate)), (db) => Effect9.sync(() => db.close())).pipe(Effect9.map((db) => makeService(db, retainedTerminalTasks)))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect9.map(() => makeService(db))));
|
|
1107
1164
|
var init_store = __esm(() => {
|
|
1108
1165
|
init_src();
|
|
1109
1166
|
init_database();
|
|
@@ -1122,7 +1179,7 @@ __export(exports_main, {
|
|
|
1122
1179
|
});
|
|
1123
1180
|
import { Effect as Effect10, ManagedRuntime, Schema as Schema11 } from "effect";
|
|
1124
1181
|
import { createHash as createHash2, timingSafeEqual } from "node:crypto";
|
|
1125
|
-
import { access as
|
|
1182
|
+
import { access as access5, chmod as chmod5 } from "node:fs/promises";
|
|
1126
1183
|
import { createServer } from "node:http";
|
|
1127
1184
|
import { platform as platform5 } from "node:os";
|
|
1128
1185
|
import { WebSocketServer } from "ws";
|
|
@@ -1258,13 +1315,13 @@ Content-Length: 0\r
|
|
|
1258
1315
|
}), runRelay = async () => {
|
|
1259
1316
|
const configuration = await Effect10.runPromise(loadEnvironmentConfiguration);
|
|
1260
1317
|
process.umask(63);
|
|
1261
|
-
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath));
|
|
1318
|
+
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
|
|
1262
1319
|
const run2 = (effect) => runtime.runPromise(effect);
|
|
1263
1320
|
await run2(Effect10.gen(function* () {
|
|
1264
1321
|
const store = yield* exports_store.Service;
|
|
1265
1322
|
yield* store.recover();
|
|
1266
1323
|
}));
|
|
1267
|
-
if (platform5() !== "win32" && await
|
|
1324
|
+
if (platform5() !== "win32" && await access5(configuration.databasePath).then(() => true).catch(() => false)) {
|
|
1268
1325
|
await chmod5(configuration.databasePath, 384);
|
|
1269
1326
|
}
|
|
1270
1327
|
const hub = new Hub;
|
|
@@ -1346,11 +1403,24 @@ Content-Length: 0\r
|
|
|
1346
1403
|
socket.close(4003, "Authentication failed");
|
|
1347
1404
|
return;
|
|
1348
1405
|
}
|
|
1349
|
-
hub.
|
|
1406
|
+
if (hub.pendingConnections() >= 64) {
|
|
1407
|
+
socket.close(4008, "Pending device connection limit reached");
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const attached = hub.attach(socket, {
|
|
1350
1411
|
...principal === "owner" ? {} : { sessionId: principal.id },
|
|
1351
1412
|
...principal === "owner" || principal.deviceId === undefined ? {} : { boundDeviceId: principal.deviceId },
|
|
1352
1413
|
...principal === "owner" ? {} : { expiresAt: principal.expiresAt }
|
|
1353
1414
|
});
|
|
1415
|
+
if (!attached) {
|
|
1416
|
+
socket.close(4008, "Credential connection limit reached");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
socket.data.stageDeadline = setTimeout(() => {
|
|
1420
|
+
if (hub.deviceId(socket) === undefined) {
|
|
1421
|
+
socket.close(4003, "Device registration timeout");
|
|
1422
|
+
}
|
|
1423
|
+
}, 5000);
|
|
1354
1424
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "Connected", serverVersion: version, connectedAt: now() })));
|
|
1355
1425
|
return;
|
|
1356
1426
|
}
|
|
@@ -1516,20 +1586,20 @@ Content-Length: 0\r
|
|
|
1516
1586
|
};
|
|
1517
1587
|
const websocketServer = new WebSocketServer({
|
|
1518
1588
|
noServer: true,
|
|
1519
|
-
maxPayload:
|
|
1589
|
+
maxPayload: maxSocketPayloadBytes,
|
|
1520
1590
|
perMessageDeflate: false
|
|
1521
1591
|
});
|
|
1522
1592
|
websocketServer.on("connection", (rawSocket) => {
|
|
1523
1593
|
const socket = rawSocket;
|
|
1524
1594
|
socket.data = {
|
|
1525
1595
|
processing: Promise.resolve(),
|
|
1526
|
-
|
|
1596
|
+
stageDeadline: undefined,
|
|
1527
1597
|
preAuthFrameReceived: false,
|
|
1528
1598
|
queuedMessages: 0,
|
|
1529
1599
|
closed: false
|
|
1530
1600
|
};
|
|
1531
1601
|
connections += 1;
|
|
1532
|
-
socket.data.
|
|
1602
|
+
socket.data.stageDeadline = setTimeout(() => {
|
|
1533
1603
|
if (!hub.isAuthorized(socket)) {
|
|
1534
1604
|
socket.close(4003, "Authentication timeout");
|
|
1535
1605
|
}
|
|
@@ -1539,6 +1609,9 @@ Content-Length: 0\r
|
|
|
1539
1609
|
alive = true;
|
|
1540
1610
|
});
|
|
1541
1611
|
const heartbeat = setInterval(() => {
|
|
1612
|
+
if (!hub.isAuthorized(socket)) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1542
1615
|
if (!alive) {
|
|
1543
1616
|
socket.terminate();
|
|
1544
1617
|
return;
|
|
@@ -1809,7 +1882,7 @@ import { accessSync, constants } from "node:fs";
|
|
|
1809
1882
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1810
1883
|
import { homedir, platform, tmpdir } from "node:os";
|
|
1811
1884
|
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
1812
|
-
import { spawn } from "node:child_process";
|
|
1885
|
+
import { execFile, spawn } from "node:child_process";
|
|
1813
1886
|
|
|
1814
1887
|
class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
|
|
1815
1888
|
}
|
|
@@ -2116,6 +2189,50 @@ var parse = async (options, stdout, onFailure) => {
|
|
|
2116
2189
|
}
|
|
2117
2190
|
};
|
|
2118
2191
|
var byteLength = (value) => new TextEncoder().encode(value).byteLength;
|
|
2192
|
+
var processExists = (pid) => {
|
|
2193
|
+
try {
|
|
2194
|
+
process.kill(-pid, 0);
|
|
2195
|
+
return true;
|
|
2196
|
+
} catch (cause) {
|
|
2197
|
+
if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ESRCH") {
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
throw cause;
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
var waitForProcessGroup = async (pid, timeoutMs) => {
|
|
2204
|
+
const deadline = Date.now() + timeoutMs;
|
|
2205
|
+
while (processExists(pid)) {
|
|
2206
|
+
if (Date.now() >= deadline) {
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2210
|
+
}
|
|
2211
|
+
return true;
|
|
2212
|
+
};
|
|
2213
|
+
var terminateWindowsTree = (pid) => new Promise((resolve) => {
|
|
2214
|
+
const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
|
|
2215
|
+
execFile(join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(pid), "/T", "/F"], { windowsHide: true }, () => resolve());
|
|
2216
|
+
});
|
|
2217
|
+
var terminateProcessTree = async (pid) => {
|
|
2218
|
+
if (platform() === "win32") {
|
|
2219
|
+
await terminateWindowsTree(pid);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
try {
|
|
2223
|
+
process.kill(-pid, "SIGTERM");
|
|
2224
|
+
} catch (cause) {
|
|
2225
|
+
if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ESRCH") {
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
throw cause;
|
|
2229
|
+
}
|
|
2230
|
+
if (await waitForProcessGroup(pid, 5000)) {
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
process.kill(-pid, "SIGKILL");
|
|
2234
|
+
await waitForProcessGroup(pid, 1000);
|
|
2235
|
+
};
|
|
2119
2236
|
var run = (options) => Effect2.tryPromise({
|
|
2120
2237
|
try: async (signal) => {
|
|
2121
2238
|
const executable = findExecutable(executables[options.provider]);
|
|
@@ -2126,72 +2243,74 @@ var run = (options) => Effect2.tryPromise({
|
|
|
2126
2243
|
});
|
|
2127
2244
|
}
|
|
2128
2245
|
const prepared = await prepareCommand(options);
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2246
|
+
try {
|
|
2247
|
+
await options.beforeSpawn?.();
|
|
2248
|
+
const [, ...arguments_] = prepared.command;
|
|
2249
|
+
const child = spawn(executable, arguments_, {
|
|
2250
|
+
cwd: options.cwd,
|
|
2251
|
+
env: {
|
|
2252
|
+
...providerEnvironment(),
|
|
2253
|
+
COHALL_PROVIDER: options.provider,
|
|
2254
|
+
COHALL_THREAD_ID: options.threadId
|
|
2255
|
+
},
|
|
2256
|
+
detached: platform() !== "win32",
|
|
2257
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2258
|
+
});
|
|
2259
|
+
const exited = new Promise((resolve, reject) => {
|
|
2260
|
+
child.once("error", reject);
|
|
2261
|
+
child.once("exit", (code) => resolve(code ?? 1));
|
|
2262
|
+
});
|
|
2263
|
+
let termination;
|
|
2264
|
+
const terminate = () => {
|
|
2265
|
+
if (child.pid === undefined || termination !== undefined) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
termination = terminateProcessTree(child.pid);
|
|
2269
|
+
};
|
|
2270
|
+
signal.addEventListener("abort", terminate, { once: true });
|
|
2271
|
+
if (signal.aborted) {
|
|
2272
|
+
terminate();
|
|
2147
2273
|
}
|
|
2148
|
-
child.
|
|
2149
|
-
|
|
2274
|
+
child.stdin.end(prepared.input);
|
|
2275
|
+
try {
|
|
2276
|
+
const [result, stderr, exitCode] = await Promise.all([
|
|
2277
|
+
parse(options, child.stdout, terminate),
|
|
2278
|
+
captureText(child.stderr, 64 * 1024),
|
|
2279
|
+
exited
|
|
2280
|
+
]);
|
|
2281
|
+
if (exitCode !== 0) {
|
|
2282
|
+
throw new ProviderRunError({
|
|
2283
|
+
provider: options.provider,
|
|
2284
|
+
message: failureMessage(executables[options.provider], stderr, exitCode),
|
|
2285
|
+
exitCode
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
if (byteLength(result.result) > 131072) {
|
|
2289
|
+
throw new ProviderRunError({
|
|
2290
|
+
provider: options.provider,
|
|
2291
|
+
message: "Provider result exceeded 128 KiB"
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
if (result.sessionId !== undefined && byteLength(result.sessionId) > 4096) {
|
|
2295
|
+
throw new ProviderRunError({
|
|
2296
|
+
provider: options.provider,
|
|
2297
|
+
message: "Provider session ID exceeded 4 KiB"
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return result;
|
|
2301
|
+
} finally {
|
|
2302
|
+
signal.removeEventListener("abort", terminate);
|
|
2150
2303
|
if (child.exitCode === null) {
|
|
2151
|
-
|
|
2304
|
+
terminate();
|
|
2305
|
+
await exited.catch(() => {
|
|
2306
|
+
return;
|
|
2307
|
+
});
|
|
2152
2308
|
}
|
|
2153
|
-
|
|
2154
|
-
killTimer.unref();
|
|
2155
|
-
};
|
|
2156
|
-
signal.addEventListener("abort", terminate, { once: true });
|
|
2157
|
-
child.stdin.end(prepared.input);
|
|
2158
|
-
try {
|
|
2159
|
-
const [result, stderr, exitCode] = await Promise.all([
|
|
2160
|
-
parse(options, child.stdout, terminate),
|
|
2161
|
-
captureText(child.stderr, 64 * 1024),
|
|
2162
|
-
exited
|
|
2163
|
-
]);
|
|
2164
|
-
if (exitCode !== 0) {
|
|
2165
|
-
throw new ProviderRunError({
|
|
2166
|
-
provider: options.provider,
|
|
2167
|
-
message: failureMessage(executables[options.provider], stderr, exitCode),
|
|
2168
|
-
exitCode
|
|
2169
|
-
});
|
|
2170
|
-
}
|
|
2171
|
-
if (byteLength(result.result) > 131072) {
|
|
2172
|
-
throw new ProviderRunError({
|
|
2173
|
-
provider: options.provider,
|
|
2174
|
-
message: "Provider result exceeded 128 KiB"
|
|
2175
|
-
});
|
|
2176
|
-
}
|
|
2177
|
-
if (result.sessionId !== undefined && byteLength(result.sessionId) > 4096) {
|
|
2178
|
-
throw new ProviderRunError({
|
|
2179
|
-
provider: options.provider,
|
|
2180
|
-
message: "Provider session ID exceeded 4 KiB"
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
return result;
|
|
2184
|
-
} finally {
|
|
2185
|
-
signal.removeEventListener("abort", terminate);
|
|
2186
|
-
if (child.exitCode === null) {
|
|
2187
|
-
terminate();
|
|
2188
|
-
await exited.catch(() => {
|
|
2309
|
+
await termination?.catch(() => {
|
|
2189
2310
|
return;
|
|
2190
2311
|
});
|
|
2191
2312
|
}
|
|
2192
|
-
|
|
2193
|
-
clearTimeout(killTimer);
|
|
2194
|
-
}
|
|
2313
|
+
} finally {
|
|
2195
2314
|
await prepared.cleanup();
|
|
2196
2315
|
}
|
|
2197
2316
|
},
|
|
@@ -2208,7 +2327,7 @@ var run = (options) => Effect2.tryPromise({
|
|
|
2208
2327
|
|
|
2209
2328
|
// apps/device/src/cli.ts
|
|
2210
2329
|
import { Effect as Effect5, Schema as Schema7 } from "effect";
|
|
2211
|
-
import { access as
|
|
2330
|
+
import { access as access3, mkdir as mkdir3, readFile as readFile3, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
|
|
2212
2331
|
import { homedir as homedir3 } from "node:os";
|
|
2213
2332
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
2214
2333
|
|
|
@@ -2342,7 +2461,8 @@ cohall thread <thread-id>
|
|
|
2342
2461
|
\`\`\`
|
|
2343
2462
|
|
|
2344
2463
|
This returns a byte-bounded recent window of prompts, final responses, and task
|
|
2345
|
-
states. Check \`truncated\`; older
|
|
2464
|
+
states. Check \`truncated\`; older entries may be outside the returned window or
|
|
2465
|
+
pruned by the relay's terminal-task retention limit.
|
|
2346
2466
|
|
|
2347
2467
|
## Input forms
|
|
2348
2468
|
|
|
@@ -2571,6 +2691,9 @@ var loadOwnerConfiguration = Effect3.tryPromise({
|
|
|
2571
2691
|
if (token === undefined || token.length === 0) {
|
|
2572
2692
|
throw new Error("No owner credential. Run this command on the relay host or set COHALL_TOKEN.");
|
|
2573
2693
|
}
|
|
2694
|
+
if (token.length < 32) {
|
|
2695
|
+
throw new Error("The Cohall relay owner token must be at least 32 characters");
|
|
2696
|
+
}
|
|
2574
2697
|
return ClientConfiguration.make({
|
|
2575
2698
|
relayUrl: normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl),
|
|
2576
2699
|
token
|
|
@@ -2704,9 +2827,20 @@ var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, thread
|
|
|
2704
2827
|
|
|
2705
2828
|
// apps/device/src/upgrade.ts
|
|
2706
2829
|
import { Schema as Schema6 } from "effect";
|
|
2707
|
-
import { execFile } from "node:child_process";
|
|
2708
|
-
import {
|
|
2709
|
-
import {
|
|
2830
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2831
|
+
import { constants as constants2 } from "node:fs";
|
|
2832
|
+
import {
|
|
2833
|
+
access as access2,
|
|
2834
|
+
chmod as chmod3,
|
|
2835
|
+
mkdir as mkdir2,
|
|
2836
|
+
readFile as readFile2,
|
|
2837
|
+
realpath as realpath2,
|
|
2838
|
+
rename as rename2,
|
|
2839
|
+
rm as rm2,
|
|
2840
|
+
stat,
|
|
2841
|
+
writeFile as writeFile3
|
|
2842
|
+
} from "node:fs/promises";
|
|
2843
|
+
import { delimiter as delimiter2, dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
2710
2844
|
import { platform as operatingSystem } from "node:os";
|
|
2711
2845
|
var packageName = "@akshar5/cohall";
|
|
2712
2846
|
var semanticVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
@@ -2744,7 +2878,7 @@ var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint
|
|
|
2744
2878
|
if (path.includes("/.bun/install/global/node_modules/")) {
|
|
2745
2879
|
return { manager: "bun", entrypoint };
|
|
2746
2880
|
}
|
|
2747
|
-
if (path.includes("/pnpm/global/")
|
|
2881
|
+
if (path.includes("/pnpm/global/")) {
|
|
2748
2882
|
return { manager: "pnpm", entrypoint };
|
|
2749
2883
|
}
|
|
2750
2884
|
const nodeModulesParent = path.slice(0, packageIndex);
|
|
@@ -2770,6 +2904,80 @@ var packageInstallCommand = (installation, target) => {
|
|
|
2770
2904
|
};
|
|
2771
2905
|
}
|
|
2772
2906
|
};
|
|
2907
|
+
var executableNames = (command) => {
|
|
2908
|
+
if (operatingSystem() !== "win32" || extname2(command).length > 0) {
|
|
2909
|
+
return [command];
|
|
2910
|
+
}
|
|
2911
|
+
return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((extension) => `${command}${extension.toLowerCase()}`);
|
|
2912
|
+
};
|
|
2913
|
+
var trustedExecutable = async (command) => {
|
|
2914
|
+
const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join3(directory, name)));
|
|
2915
|
+
let selected;
|
|
2916
|
+
for (const path of candidates) {
|
|
2917
|
+
const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
|
|
2918
|
+
if (available2) {
|
|
2919
|
+
selected = path;
|
|
2920
|
+
break;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if (selected === undefined) {
|
|
2924
|
+
throw new Error(`Could not find ${command} on PATH`);
|
|
2925
|
+
}
|
|
2926
|
+
const canonical = await realpath2(selected);
|
|
2927
|
+
if (operatingSystem() === "win32") {
|
|
2928
|
+
return canonical;
|
|
2929
|
+
}
|
|
2930
|
+
const uid = process.getuid?.();
|
|
2931
|
+
for (let path = canonical;; path = dirname2(path)) {
|
|
2932
|
+
const metadata = await stat(path);
|
|
2933
|
+
if ((metadata.mode & 18) !== 0) {
|
|
2934
|
+
throw new Error(`Refusing executable beneath group- or world-writable path ${path}`);
|
|
2935
|
+
}
|
|
2936
|
+
if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid) {
|
|
2937
|
+
throw new Error(`Refusing executable owned by another user at ${path}`);
|
|
2938
|
+
}
|
|
2939
|
+
const parent = dirname2(path);
|
|
2940
|
+
if (parent === path) {
|
|
2941
|
+
break;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
return canonical;
|
|
2945
|
+
};
|
|
2946
|
+
var trustedServices = async (services, resolveExecutable) => {
|
|
2947
|
+
const resolved = new Map;
|
|
2948
|
+
const invocation = async (value) => {
|
|
2949
|
+
let command = resolved.get(value.command);
|
|
2950
|
+
if (command === undefined) {
|
|
2951
|
+
command = resolveExecutable(value.command);
|
|
2952
|
+
resolved.set(value.command, command);
|
|
2953
|
+
}
|
|
2954
|
+
return { ...value, command: await command };
|
|
2955
|
+
};
|
|
2956
|
+
const secured = [];
|
|
2957
|
+
for (const service of services) {
|
|
2958
|
+
let check;
|
|
2959
|
+
try {
|
|
2960
|
+
check = await invocation(service.check);
|
|
2961
|
+
} catch (cause) {
|
|
2962
|
+
if (cause instanceof Error && cause.message.startsWith("Could not find ")) {
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
throw cause;
|
|
2966
|
+
}
|
|
2967
|
+
secured.push({
|
|
2968
|
+
...service,
|
|
2969
|
+
check,
|
|
2970
|
+
...service.entrypoint === undefined ? {} : {
|
|
2971
|
+
entrypoint: {
|
|
2972
|
+
...service.entrypoint,
|
|
2973
|
+
inspect: await invocation(service.entrypoint.inspect)
|
|
2974
|
+
}
|
|
2975
|
+
},
|
|
2976
|
+
restart: await Promise.all(service.restart.map(invocation))
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
return secured;
|
|
2980
|
+
};
|
|
2773
2981
|
var serviceCandidates = (runtimePlatform, uid) => {
|
|
2774
2982
|
if (runtimePlatform === "linux") {
|
|
2775
2983
|
const systemd = (manager, unit, device) => {
|
|
@@ -2831,11 +3039,11 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
2831
3039
|
},
|
|
2832
3040
|
restart: [
|
|
2833
3041
|
{
|
|
2834
|
-
command: "schtasks",
|
|
3042
|
+
command: "schtasks.exe",
|
|
2835
3043
|
arguments: ["/End", "/TN", "Cohall Device"],
|
|
2836
3044
|
allowFailure: true
|
|
2837
3045
|
},
|
|
2838
|
-
{ command: "schtasks", arguments: ["/Run", "/TN", "Cohall Device"] }
|
|
3046
|
+
{ command: "schtasks.exe", arguments: ["/Run", "/TN", "Cohall Device"] }
|
|
2839
3047
|
]
|
|
2840
3048
|
}
|
|
2841
3049
|
];
|
|
@@ -2844,7 +3052,7 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
2844
3052
|
};
|
|
2845
3053
|
var defaultRunner = {
|
|
2846
3054
|
run: (command, arguments_, timeoutMs = 300000) => new Promise((resolve2) => {
|
|
2847
|
-
|
|
3055
|
+
execFile2(command, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
|
|
2848
3056
|
resolve2({
|
|
2849
3057
|
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
2850
3058
|
stdout,
|
|
@@ -2953,9 +3161,10 @@ var restartServices = async (runner, services, statePath, state, preserveDeviceR
|
|
|
2953
3161
|
var reportedRestartServices = (receipt) => receipt.restartingService === undefined || receipt.restartedServices.includes(receipt.restartingService) ? receipt.restartedServices : [...receipt.restartedServices, receipt.restartingService];
|
|
2954
3162
|
var upgrade = async (options) => {
|
|
2955
3163
|
const runner = options.runner ?? defaultRunner;
|
|
3164
|
+
const resolveExecutable = options.resolveExecutable ?? trustedExecutable;
|
|
2956
3165
|
const runtimePlatform = options.platform ?? process.platform;
|
|
2957
3166
|
const uid = options.uid ?? process.getuid?.();
|
|
2958
|
-
const candidates = serviceCandidates(runtimePlatform, uid);
|
|
3167
|
+
const candidates = await trustedServices(serviceCandidates(runtimePlatform, uid), resolveExecutable);
|
|
2959
3168
|
const statePath = options.statePath ?? restartReceiptPath();
|
|
2960
3169
|
const previous = await readReceipt(statePath);
|
|
2961
3170
|
if (previous?.version === options.currentVersion) {
|
|
@@ -3035,7 +3244,7 @@ var upgrade = async (options) => {
|
|
|
3035
3244
|
dry_run: true
|
|
3036
3245
|
};
|
|
3037
3246
|
}
|
|
3038
|
-
await checked(runner, install);
|
|
3247
|
+
await checked(runner, { ...install, command: await resolveExecutable(install.command) });
|
|
3039
3248
|
const nextVersion = await installedVersion(entrypoint);
|
|
3040
3249
|
if (target !== "latest" && nextVersion !== target) {
|
|
3041
3250
|
throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
|
|
@@ -3225,10 +3434,10 @@ var readInput = async (arguments_, name) => {
|
|
|
3225
3434
|
throw new Error(`Use either --${name} or --${name}-file, not both`);
|
|
3226
3435
|
}
|
|
3227
3436
|
if (path !== undefined) {
|
|
3228
|
-
if (!await
|
|
3437
|
+
if (!await access3(path).then(() => true).catch(() => false)) {
|
|
3229
3438
|
throw new Error(`File does not exist: ${path}`);
|
|
3230
3439
|
}
|
|
3231
|
-
if ((await
|
|
3440
|
+
if ((await stat2(path)).size > 131072) {
|
|
3232
3441
|
throw new Error(`${name} file exceeds 128 KiB`);
|
|
3233
3442
|
}
|
|
3234
3443
|
return readFile3(path, "utf8");
|
|
@@ -3261,10 +3470,10 @@ var timeout = (arguments_) => {
|
|
|
3261
3470
|
};
|
|
3262
3471
|
var pairingToken = async (arguments_) => {
|
|
3263
3472
|
const path = option(arguments_, "token-file");
|
|
3264
|
-
if (path !== undefined && !await
|
|
3473
|
+
if (path !== undefined && !await access3(path).then(() => true).catch(() => false)) {
|
|
3265
3474
|
throw new Error(`Pairing token file does not exist: ${path}`);
|
|
3266
3475
|
}
|
|
3267
|
-
if (path !== undefined && (await
|
|
3476
|
+
if (path !== undefined && (await stat2(path)).size > 512) {
|
|
3268
3477
|
throw new Error("Pairing token file exceeds 512 bytes");
|
|
3269
3478
|
}
|
|
3270
3479
|
const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile3(path, "utf8")).trim();
|
|
@@ -3351,14 +3560,23 @@ var runCli = async (command, raw) => {
|
|
|
3351
3560
|
const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
|
|
3352
3561
|
const deviceToken = paired.credentials.find((credential) => credential.session.role === "device")?.token;
|
|
3353
3562
|
const deviceId = paired.credentials.find((credential) => credential.session.role === "device")?.session.deviceId;
|
|
3354
|
-
|
|
3563
|
+
const clientOnly = arguments_.options.has("client-only");
|
|
3564
|
+
if (clientOnly && clientToken === undefined) {
|
|
3565
|
+
throw new Error("The pairing credential did not include client access");
|
|
3566
|
+
}
|
|
3567
|
+
if (!clientOnly && deviceToken === undefined) {
|
|
3355
3568
|
throw new Error("The pairing credential did not include device access");
|
|
3356
3569
|
}
|
|
3570
|
+
const {
|
|
3571
|
+
clientToken: _retainedClientToken,
|
|
3572
|
+
deviceToken: _retainedDeviceToken,
|
|
3573
|
+
...configurationDraft
|
|
3574
|
+
} = draft;
|
|
3357
3575
|
const configuration2 = StoredConfiguration.make({
|
|
3358
|
-
...
|
|
3359
|
-
deviceId: deviceId ??
|
|
3576
|
+
...configurationDraft,
|
|
3577
|
+
deviceId: clientOnly ? configurationDraft.deviceId : deviceId ?? configurationDraft.deviceId,
|
|
3360
3578
|
...clientToken === undefined ? {} : { clientToken },
|
|
3361
|
-
...deviceToken === undefined ? {} : { deviceToken }
|
|
3579
|
+
...clientOnly || deviceToken === undefined ? {} : { deviceToken }
|
|
3362
3580
|
});
|
|
3363
3581
|
await writeStoredConfiguration(configuration2);
|
|
3364
3582
|
print({
|
|
@@ -3366,7 +3584,7 @@ var runCli = async (command, raw) => {
|
|
|
3366
3584
|
config_path: configurationPath(),
|
|
3367
3585
|
relay_url: configuration2.relayUrl,
|
|
3368
3586
|
device_id: configuration2.deviceId,
|
|
3369
|
-
roles: paired.credentials.map((credential) => credential.session.role),
|
|
3587
|
+
roles: clientOnly ? paired.credentials.map((credential) => credential.session.role).filter((role) => role === "client") : paired.credentials.map((credential) => credential.session.role),
|
|
3370
3588
|
workspaces: configuration2.workspaces
|
|
3371
3589
|
});
|
|
3372
3590
|
return;
|
|
@@ -3618,11 +3836,11 @@ var runCli = async (command, raw) => {
|
|
|
3618
3836
|
// apps/device/src/daemon.ts
|
|
3619
3837
|
init_src();
|
|
3620
3838
|
import { Effect as Effect6, Schedule as Schedule2, Schema as Schema8 } from "effect";
|
|
3621
|
-
import {
|
|
3839
|
+
import { constants as constants3 } from "node:fs";
|
|
3840
|
+
import { open, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
3622
3841
|
import { arch, hostname as hostname2, platform as platform3 } from "node:os";
|
|
3623
|
-
import { basename, isAbsolute as
|
|
3842
|
+
import { basename, isAbsolute as isAbsolute3, relative } from "node:path";
|
|
3624
3843
|
import { WebSocket } from "ws";
|
|
3625
|
-
var maxRelayPayloadBytes = 256 * 1024;
|
|
3626
3844
|
var maxQueuedRelayMessages = 8;
|
|
3627
3845
|
|
|
3628
3846
|
class DeviceConnectionError extends Schema8.TaggedErrorClass()("Device.ConnectionError", { message: Schema8.String }) {
|
|
@@ -3691,13 +3909,42 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
3691
3909
|
const candidate = await realpath3(selected);
|
|
3692
3910
|
const allowed = configuration.workspaces.some((root) => {
|
|
3693
3911
|
const child = relative(root, candidate);
|
|
3694
|
-
return child === "" || !child.startsWith("..") && !
|
|
3912
|
+
return child === "" || !child.startsWith("..") && !isAbsolute3(child);
|
|
3695
3913
|
});
|
|
3696
3914
|
if (!allowed) {
|
|
3697
3915
|
throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
|
|
3698
3916
|
}
|
|
3699
3917
|
return candidate;
|
|
3700
3918
|
};
|
|
3919
|
+
var openAllowedWorkspace = async (configuration, requested) => {
|
|
3920
|
+
const path = await allowedWorkspace(configuration, requested);
|
|
3921
|
+
const operatingSystem2 = platform3();
|
|
3922
|
+
const flags = constants3.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
|
|
3923
|
+
const handle = await open(path, flags);
|
|
3924
|
+
const identity = await handle.stat();
|
|
3925
|
+
if (!identity.isDirectory()) {
|
|
3926
|
+
await handle.close();
|
|
3927
|
+
throw new Error(`Workspace ${path} is not a directory`);
|
|
3928
|
+
}
|
|
3929
|
+
const cwd = operatingSystem2 === "linux" ? `/proc/self/fd/${handle.fd}` : operatingSystem2 === "darwin" ? `/dev/fd/${handle.fd}` : path;
|
|
3930
|
+
return {
|
|
3931
|
+
cwd,
|
|
3932
|
+
validate: async () => {
|
|
3933
|
+
const current = await handle.stat();
|
|
3934
|
+
if (!current.isDirectory() || current.dev !== identity.dev || current.ino !== identity.ino) {
|
|
3935
|
+
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
3936
|
+
}
|
|
3937
|
+
if (operatingSystem2 === "win32") {
|
|
3938
|
+
const currentPath = await realpath3(path);
|
|
3939
|
+
const currentPathIdentity = await stat3(currentPath);
|
|
3940
|
+
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
3941
|
+
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
},
|
|
3945
|
+
close: () => handle.close()
|
|
3946
|
+
};
|
|
3947
|
+
};
|
|
3701
3948
|
var promptFor = (task, deviceName) => {
|
|
3702
3949
|
const context = task.context === undefined ? "" : `
|
|
3703
3950
|
|
|
@@ -3746,8 +3993,8 @@ var execute = (configuration, state, task) => {
|
|
|
3746
3993
|
state.tasks.set(task.id, controller);
|
|
3747
3994
|
send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
|
|
3748
3995
|
const workflow = Effect6.gen(function* () {
|
|
3749
|
-
const
|
|
3750
|
-
try: () =>
|
|
3996
|
+
const workspace = yield* Effect6.tryPromise({
|
|
3997
|
+
try: () => openAllowedWorkspace(configuration, task.workspace),
|
|
3751
3998
|
catch: (cause) => new ProviderRunError({
|
|
3752
3999
|
provider: task.provider,
|
|
3753
4000
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -3758,11 +4005,14 @@ var execute = (configuration, state, task) => {
|
|
|
3758
4005
|
provider: task.provider,
|
|
3759
4006
|
threadId: task.threadId,
|
|
3760
4007
|
prompt: promptFor(task, configuration.name),
|
|
3761
|
-
cwd,
|
|
4008
|
+
cwd: workspace.cwd,
|
|
4009
|
+
beforeSpawn: workspace.validate,
|
|
3762
4010
|
...sessionId === undefined ? {} : { sessionId },
|
|
3763
4011
|
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
3764
4012
|
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
3765
|
-
})
|
|
4013
|
+
}).pipe(Effect6.ensuring(Effect6.promise(() => workspace.close().catch(() => {
|
|
4014
|
+
return;
|
|
4015
|
+
}))));
|
|
3766
4016
|
});
|
|
3767
4017
|
Effect6.runPromise(workflow, { signal: controller.signal }).then((result) => {
|
|
3768
4018
|
if (result.sessionId !== undefined) {
|
|
@@ -3823,7 +4073,7 @@ var cancel = (state, taskId) => {
|
|
|
3823
4073
|
var connect = (configuration, state) => Effect6.tryPromise({
|
|
3824
4074
|
try: (signal) => new Promise((complete) => {
|
|
3825
4075
|
const socket = new WebSocket(socketUrl(configuration), {
|
|
3826
|
-
maxPayload:
|
|
4076
|
+
maxPayload: maxSocketPayloadBytes,
|
|
3827
4077
|
perMessageDeflate: false
|
|
3828
4078
|
});
|
|
3829
4079
|
const heartbeat = setInterval(() => {
|
|
@@ -4014,12 +4264,12 @@ var main = async () => {
|
|
|
4014
4264
|
console.log(version);
|
|
4015
4265
|
return;
|
|
4016
4266
|
}
|
|
4267
|
+
const commandArguments = process.argv.slice(3);
|
|
4268
|
+
if (commandArguments.length === 1 && ["--help", "-h", "help"].includes(commandArguments[0] ?? "")) {
|
|
4269
|
+
printHelp();
|
|
4270
|
+
return;
|
|
4271
|
+
}
|
|
4017
4272
|
if (["device", "relay", "mcp"].includes(command) && process.argv.length > 3) {
|
|
4018
|
-
const arguments_ = process.argv.slice(3);
|
|
4019
|
-
if (arguments_.length === 1 && ["--help", "-h", "help"].includes(arguments_[0] ?? "")) {
|
|
4020
|
-
printHelp();
|
|
4021
|
-
return;
|
|
4022
|
-
}
|
|
4023
4273
|
throw new Error(`${command} does not accept arguments`);
|
|
4024
4274
|
}
|
|
4025
4275
|
if (command === "relay") {
|
|
@@ -4037,11 +4287,11 @@ var main = async () => {
|
|
|
4037
4287
|
await runMcp(configuration);
|
|
4038
4288
|
return;
|
|
4039
4289
|
}
|
|
4040
|
-
await runCli(command,
|
|
4290
|
+
await runCli(command, commandArguments);
|
|
4041
4291
|
};
|
|
4042
4292
|
await main().catch((cause) => {
|
|
4043
4293
|
console.error(JSON.stringify({ error: cause instanceof Error ? cause.message : String(cause) }, null, 2));
|
|
4044
4294
|
process.exitCode = 1;
|
|
4045
4295
|
});
|
|
4046
4296
|
|
|
4047
|
-
//# debugId=
|
|
4297
|
+
//# debugId=1A046C86E8783E4364756E2164756E21
|