@akshar5/cohall 0.5.0 → 0.5.2

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.2](https://github.com/AksharP5/cohall/compare/v0.5.1...v0.5.2) (2026-08-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **device:** support confined and Homebrew upgrades ([#54](https://github.com/AksharP5/cohall/issues/54)) ([80759fd](https://github.com/AksharP5/cohall/commit/80759fdaefa03fd8f6c7b6fd0a3f72b09c3b3d39))
9
+
10
+ ## [0.5.1](https://github.com/AksharP5/cohall/compare/v0.5.0...v0.5.1) (2026-08-10)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **relay:** reject upgrades for legacy daemons ([#52](https://github.com/AksharP5/cohall/issues/52)) ([d4ab660](https://github.com/AksharP5/cohall/commit/d4ab660cce5489f6f572e221e71fc4ff6a412287))
16
+
3
17
  ## [0.5.0](https://github.com/AksharP5/cohall/compare/v0.4.10...v0.5.0) (2026-08-10)
4
18
 
5
19
 
package/README.md CHANGED
@@ -219,6 +219,8 @@ cohall upgrades abandon <operation-id>
219
219
 
220
220
  Upgrade requests are durable for offline devices and run after active delegated
221
221
  work. This is a typed owner-only maintenance operation, not remote shell access.
222
+ Every device must already run Cohall 0.5.0 or newer; upgrade older installations
223
+ individually once before using all-device upgrades.
222
224
  Each target must use a global npm, Bun, or pnpm installation that can upgrade
223
225
  itself. Preview the targets with `cohall upgrade --all --dry-run`, or pin an
224
226
  exact release with `--to 1.2.3`. `cohall upgrades` returns the 50 newest results.
package/bin/cohall.js CHANGED
@@ -17,9 +17,40 @@ 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.5.0", 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)), count, 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$/, {
20
+ var version = "0.5.2", 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)), count, 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
- })).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, 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()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
22
+ })).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
23
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
24
+ if (match === null) {
25
+ return;
26
+ }
27
+ const core = match.slice(1, 4).map(Number);
28
+ if (core.length !== 3 || core.some((part) => !Number.isSafeInteger(part))) {
29
+ return;
30
+ }
31
+ return [core[0] ?? 0, core[1] ?? 0, core[2] ?? 0, match[4] !== undefined];
32
+ }, supportsDeviceOperations = (candidate) => {
33
+ if (candidate === "0.0.0-development") {
34
+ return true;
35
+ }
36
+ const current = parsedVersion(candidate);
37
+ const minimum = parsedVersion(minimumDeviceOperationVersion);
38
+ if (current === undefined || minimum === undefined) {
39
+ return false;
40
+ }
41
+ for (let index = 0;index < 3; index += 1) {
42
+ if (current[index] !== minimum[index]) {
43
+ return (current[index] ?? 0) > (minimum[index] ?? 0);
44
+ }
45
+ }
46
+ return !current[3];
47
+ }, assertDeviceOperationSupport = (devices) => {
48
+ const unsupported = devices.filter((device) => !supportsDeviceOperations(device.version));
49
+ if (unsupported.length === 0) {
50
+ return;
51
+ }
52
+ throw new Error(`All-device upgrades require Cohall ${minimumDeviceOperationVersion} or newer on every device. Upgrade individually first: ${unsupported.map((device) => `${device.name} (${device.version})`).join(", ")}`);
53
+ }, Thread, Message, Task, upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, 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()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
23
54
  var init_src = __esm(() => {
24
55
  maxSocketPayloadBytes = 1024 * 1024;
25
56
  count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
@@ -820,6 +851,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
820
851
  if (devices.length > 256) {
821
852
  throw new Error("All-device operations support at most 256 registered devices");
822
853
  }
854
+ assertDeviceOperationSupport(devices);
823
855
  for (const device of devices) {
824
856
  const outstanding = db.query(`SELECT COUNT(*) AS count FROM device_operations
825
857
  WHERE target_device_id = ? AND status IN ('queued', 'assigned', 'running')`).get(device.id);
@@ -3378,7 +3410,7 @@ import {
3378
3410
  stat,
3379
3411
  writeFile as writeFile3
3380
3412
  } from "node:fs/promises";
3381
- import { delimiter as delimiter2, dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
3413
+ import { delimiter as delimiter2, dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3, relative } from "node:path";
3382
3414
  import { platform as operatingSystem } from "node:os";
3383
3415
  var packageName = "@akshar5/cohall";
3384
3416
  var semanticVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -3448,7 +3480,19 @@ var executableNames = (command) => {
3448
3480
  }
3449
3481
  return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((extension) => `${command}${extension.toLowerCase()}`);
3450
3482
  };
3451
- var trustedExecutable = async (command) => {
3483
+ var containsPath = (root, path) => {
3484
+ const child = relative(root, path);
3485
+ return child === "" || !child.startsWith("..") && !isAbsolute2(child);
3486
+ };
3487
+ var linuxSystemExecutableRoots = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/nix/store"];
3488
+ var macosHomebrewRoots = ["/opt/homebrew", "/usr/local"];
3489
+ var macosAdminGroup = 80;
3490
+ var isTrustedSystemExecutablePath = (platform3, canonical) => platform3 === "linux" && linuxSystemExecutableRoots.some((root) => containsPath(root, canonical));
3491
+ var isTrustedGroupWritablePath = (options) => {
3492
+ const writableRoot = options.writableRoot;
3493
+ return options.platform === "darwin" && writableRoot !== undefined && options.uid !== undefined && options.ownerUid === options.uid && options.ownerGid === macosAdminGroup && macosHomebrewRoots.some((root) => containsPath(root, writableRoot)) && containsPath(writableRoot, options.canonical) && containsPath(writableRoot, options.path);
3494
+ };
3495
+ var trustedExecutable = async (command, options = {}) => {
3452
3496
  const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join3(directory, name)));
3453
3497
  let selected;
3454
3498
  for (const path of candidates) {
@@ -3466,12 +3510,23 @@ var trustedExecutable = async (command) => {
3466
3510
  return canonical;
3467
3511
  }
3468
3512
  const uid = process.getuid?.();
3513
+ const writableRoot = options.writableRoot === undefined ? undefined : await realpath2(options.writableRoot);
3514
+ const systemExecutable = isTrustedSystemExecutablePath(operatingSystem(), canonical);
3469
3515
  for (let path = canonical;; path = dirname2(path)) {
3470
3516
  const metadata = await stat(path);
3471
- if ((metadata.mode & 18) !== 0) {
3517
+ const trustedGroupWritablePath = isTrustedGroupWritablePath({
3518
+ platform: operatingSystem(),
3519
+ canonical,
3520
+ writableRoot,
3521
+ path,
3522
+ uid,
3523
+ ownerUid: metadata.uid,
3524
+ ownerGid: metadata.gid
3525
+ });
3526
+ if ((metadata.mode & 2) !== 0 || (metadata.mode & 16) !== 0 && !trustedGroupWritablePath) {
3472
3527
  throw new Error(`Refusing executable beneath group- or world-writable path ${path}`);
3473
3528
  }
3474
- if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid) {
3529
+ if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !systemExecutable) {
3475
3530
  throw new Error(`Refusing executable owned by another user at ${path}`);
3476
3531
  }
3477
3532
  const parent = dirname2(path);
@@ -3782,7 +3837,8 @@ var upgrade = async (options) => {
3782
3837
  dry_run: true
3783
3838
  };
3784
3839
  }
3785
- await checked(runner, { ...install, command: await resolveExecutable(install.command) });
3840
+ const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
3841
+ await checked(runner, { ...install, command: installExecutable });
3786
3842
  const nextVersion = await installedVersion(entrypoint);
3787
3843
  if (target !== "latest" && nextVersion !== target) {
3788
3844
  throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
@@ -4658,6 +4714,7 @@ var runCli = async (command, raw) => {
4658
4714
  const restart = !arguments_.options.has("no-restart");
4659
4715
  if (arguments_.options.has("dry-run")) {
4660
4716
  const devices = await Effect7.runPromise(relay2.devices());
4717
+ assertDeviceOperationSupport(devices);
4661
4718
  print({
4662
4719
  dry_run: true,
4663
4720
  requested_version: normalizedTarget,
@@ -5010,7 +5067,7 @@ import { Effect as Effect8, Schedule as Schedule2, Schema as Schema9 } from "eff
5010
5067
  import { constants as constants3 } from "node:fs";
5011
5068
  import { open, realpath as realpath5, stat as stat4 } from "node:fs/promises";
5012
5069
  import { arch, hostname as hostname3, platform as platform3 } from "node:os";
5013
- import { basename as basename2, isAbsolute as isAbsolute3, relative } from "node:path";
5070
+ import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
5014
5071
  import { WebSocket as WebSocket2 } from "ws";
5015
5072
  var maxQueuedRelayMessages = 8;
5016
5073
 
@@ -5079,7 +5136,7 @@ var allowedWorkspace = async (configuration, requested) => {
5079
5136
  }
5080
5137
  const candidate = await realpath5(selected);
5081
5138
  const allowed = configuration.workspaces.some((root) => {
5082
- const child = relative(root, candidate);
5139
+ const child = relative2(root, candidate);
5083
5140
  return child === "" || !child.startsWith("..") && !isAbsolute3(child);
5084
5141
  });
5085
5142
  if (!allowed) {
@@ -5557,4 +5614,4 @@ await main().catch((cause) => {
5557
5614
  process.exitCode = 1;
5558
5615
  });
5559
5616
 
5560
- //# debugId=42161A8EF448D66964756E2164756E21
5617
+ //# debugId=450AEED33D42A83A64756E2164756E21