@agentrific/server 0.5.1 → 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,13 @@
1
1
  # @agentrific/server
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - fe18d59: Add automatic updates for signed macOS desktop releases and an optional bundled local server that continues in the tray. Defer update installation and server shutdown while local agents are active. Host the web app and architecture-specific update feeds at app.agentrific.com.
8
+
9
+ Keep local control sockets usable with long data directory paths and allow the hosted web app to connect to paired servers.
10
+
3
11
  ## 0.5.1
4
12
 
5
13
  ### Patch Changes
package/cli.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.5.1",
33
+ version: "0.5.2",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -203,6 +203,21 @@ var init_updates = __esm({
203
203
  }
204
204
  });
205
205
 
206
+ // ../../packages/contracts/src/control-socket.ts
207
+ import { createHash } from "node:crypto";
208
+ import { join, resolve } from "node:path";
209
+ function controlSocketPath(directory, uid = process.getuid?.()) {
210
+ const direct = join(resolve(directory), "control.sock");
211
+ if (Buffer.byteLength(direct) < 100) return direct;
212
+ if (uid === void 0) throw new Error("Local control sockets require macOS or Linux.");
213
+ const digest2 = createHash("sha256").update(resolve(directory)).digest("hex").slice(0, 32);
214
+ return join("/tmp", `agentrific-${uid}`, `${digest2}.sock`);
215
+ }
216
+ var init_control_socket = __esm({
217
+ "../../packages/contracts/src/control-socket.ts"() {
218
+ }
219
+ });
220
+
206
221
  // ../../packages/contracts/src/index.ts
207
222
  import { z as z2 } from "zod";
208
223
  function cronRange(base, min, max, stepped) {
@@ -590,7 +605,7 @@ var init_src = __esm({
590
605
 
591
606
  // src/control.ts
592
607
  import { createServer, createConnection } from "node:net";
593
- import { chmod, mkdir as mkdir2, readFile, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
608
+ import { chmod, mkdir as mkdir2, lstat, readFile, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
594
609
  import path2 from "node:path";
595
610
  import { Effect as Effect2 } from "effect";
596
611
  import { z as z3 } from "zod";
@@ -635,8 +650,14 @@ async function acquireServerLock(directory) {
635
650
  await unlink2(filename);
636
651
  };
637
652
  }
638
- async function startControl(directory, security2, status, maintenance2) {
639
- const filename = path2.join(directory, "control.sock");
653
+ async function startControl(directory, security2, status, maintenance2, shutdown) {
654
+ const filename = controlSocketPath(directory);
655
+ const socketDirectory = path2.dirname(filename);
656
+ await mkdir2(socketDirectory, { recursive: true, mode: 448 });
657
+ const metadata = await lstat(socketDirectory);
658
+ if (!metadata.isDirectory() || metadata.uid !== process.getuid?.())
659
+ throw new Error("Local control directory must be private and owned by this user.");
660
+ await chmod(socketDirectory, 448);
640
661
  await unlink2(filename).catch((error) => {
641
662
  if (!z3.object({ code: z3.literal("ENOENT") }).safeParse(error).success) throw error;
642
663
  });
@@ -656,6 +677,13 @@ async function startControl(directory, security2, status, maintenance2) {
656
677
  const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
657
678
  let result;
658
679
  switch (request.command) {
680
+ case "shutdown":
681
+ if (!shutdown || !maintenance2?.status().drained)
682
+ throw new Error("Drain the server before shutdown");
683
+ socket.end(JSON.stringify({ result: { shutdown: true } }) + "\n", () => {
684
+ setImmediate(() => void shutdown());
685
+ });
686
+ return;
659
687
  case "pair":
660
688
  result = security2.createPairing();
661
689
  break;
@@ -682,20 +710,20 @@ async function startControl(directory, security2, status, maintenance2) {
682
710
  });
683
711
  socket.on("error", () => socket.destroy());
684
712
  });
685
- await new Promise((resolve, reject) => {
713
+ await new Promise((resolve2, reject) => {
686
714
  server.once("error", reject);
687
- server.listen(filename, resolve);
715
+ server.listen(filename, resolve2);
688
716
  });
689
717
  await chmod(filename, 384);
690
718
  return async () => {
691
719
  await new Promise(
692
- (resolve, reject) => server.close((error) => error ? reject(error) : resolve())
720
+ (resolve2, reject) => server.close((error) => error ? reject(error) : resolve2())
693
721
  );
694
722
  };
695
723
  }
696
724
  function controlRequest(directory, command) {
697
- return new Promise((resolve, reject) => {
698
- const socket = createConnection(path2.join(directory, "control.sock"));
725
+ return new Promise((resolve2, reject) => {
726
+ const socket = createConnection(controlSocketPath(directory));
699
727
  socket.setTimeout(command.command === "maintenance" ? 65e3 : 5e3, () => {
700
728
  socket.destroy();
701
729
  reject(new Error("Server did not respond."));
@@ -711,7 +739,7 @@ function controlRequest(directory, command) {
711
739
  const result = z3.object({ result: ControlResultSchema.optional(), error: z3.string().optional() }).parse(JSON.parse(response));
712
740
  if (result.error) throw new Error(result.error);
713
741
  if (!result.result) throw new Error("Server returned an empty response.");
714
- resolve(result.result);
742
+ resolve2(result.result);
715
743
  } catch (error) {
716
744
  reject(error);
717
745
  }
@@ -722,6 +750,7 @@ var MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
722
750
  var init_control = __esm({
723
751
  "src/control.ts"() {
724
752
  "use strict";
753
+ init_control_socket();
725
754
  init_src();
726
755
  MaintenanceSchema = z3.object({
727
756
  active: z3.boolean(),
@@ -731,6 +760,7 @@ var init_control = __esm({
731
760
  ControlStatusSchema = z3.object({
732
761
  serverId: z3.string().uuid(),
733
762
  publicUrl: z3.string().optional(),
763
+ localUrl: z3.string().optional(),
734
764
  serverVersion: z3.string().optional(),
735
765
  connectedDevices: z3.number().optional(),
736
766
  connectedSockets: z3.number().optional(),
@@ -748,12 +778,14 @@ var init_control = __esm({
748
778
  DeviceSchema.array(),
749
779
  ControlStatusSchema,
750
780
  z3.object({ revoked: z3.boolean() }),
781
+ z3.object({ shutdown: z3.literal(true) }),
751
782
  MaintenanceSchema
752
783
  ]);
753
784
  ControlRequest = z3.discriminatedUnion("command", [
754
785
  z3.object({ command: z3.literal("pair") }),
755
786
  z3.object({ command: z3.literal("devices") }),
756
787
  z3.object({ command: z3.literal("status") }),
788
+ z3.object({ command: z3.literal("shutdown") }),
757
789
  z3.object({ command: z3.literal("revoke"), id: z3.string().uuid() }),
758
790
  z3.object({
759
791
  command: z3.literal("maintenance"),
@@ -855,7 +887,7 @@ function printManualPairing(invitation) {
855
887
  }
856
888
  function waitForManualPairing(invitation) {
857
889
  console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
858
- return new Promise((resolve) => {
890
+ return new Promise((resolve2) => {
859
891
  const input = process.stdin;
860
892
  const previousRaw = input.isRaw;
861
893
  const timeout = setTimeout(
@@ -875,7 +907,7 @@ function waitForManualPairing(invitation) {
875
907
  process.removeListener("SIGTERM", terminated);
876
908
  input.setRawMode(previousRaw);
877
909
  input.pause();
878
- resolve();
910
+ resolve2();
879
911
  }
880
912
  function interrupted() {
881
913
  process.exitCode = 130;
@@ -1542,7 +1574,7 @@ var init_event_server = __esm({
1542
1574
  });
1543
1575
 
1544
1576
  // src/security.ts
1545
- import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
1577
+ import { createHash as createHash2, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
1546
1578
  import { Effect as Effect5 } from "effect";
1547
1579
  import { z as z8 } from "zod";
1548
1580
  async function registerSecurity(app2, security2, version, allowedOrigins2) {
@@ -1630,7 +1662,7 @@ var init_security = __esm({
1630
1662
  init_src();
1631
1663
  init_pairing();
1632
1664
  secret = () => randomBytes(32).toString("hex");
1633
- digest = (value) => createHash("sha256").update(value).digest("hex");
1665
+ digest = (value) => createHash2("sha256").update(value).digest("hex");
1634
1666
  ServerSecurity = class {
1635
1667
  constructor(store2, serverId) {
1636
1668
  this.store = store2;
@@ -1756,7 +1788,7 @@ var init_maintenance = __esm({
1756
1788
  this.enter();
1757
1789
  const deadline = Date.now() + timeoutMs;
1758
1790
  while (!this.status().drained && Date.now() < deadline)
1759
- await new Promise((resolve) => setTimeout(resolve, 50));
1791
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1760
1792
  return this.status();
1761
1793
  }
1762
1794
  attach(app2) {
@@ -4183,7 +4215,7 @@ var init_store = __esm({
4183
4215
  });
4184
4216
 
4185
4217
  // src/worktree-cleanup.ts
4186
- import { lstat, rm as rm2 } from "node:fs/promises";
4218
+ import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
4187
4219
  import path10 from "node:path";
4188
4220
  import { Effect as Effect15 } from "effect";
4189
4221
  async function removeRunFiles(dataRoot2, sessionId) {
@@ -4209,7 +4241,7 @@ var init_worktree_cleanup = __esm({
4209
4241
  async remove(session, discardChanges = true) {
4210
4242
  if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
4211
4243
  const workspace = path10.resolve(session.workspacePath);
4212
- const info = await lstat(workspace).catch((error) => {
4244
+ const info = await lstat2(workspace).catch((error) => {
4213
4245
  if (error.code === "ENOENT") return void 0;
4214
4246
  throw error;
4215
4247
  });
@@ -4513,6 +4545,10 @@ async function withSessionTaskLock(id, operation) {
4513
4545
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
4514
4546
  return withTaskLock(lockId, operation);
4515
4547
  }
4548
+ function localServerUrl() {
4549
+ const address = z18.object({ port: z18.number() }).safeParse(app.server.address());
4550
+ return address.success ? `http://${host}:${address.data.port}` : void 0;
4551
+ }
4516
4552
  var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
4517
4553
  var init_index = __esm({
4518
4554
  async "src/index.ts"() {
@@ -4589,6 +4625,7 @@ var init_index = __esm({
4589
4625
  allowedOrigins = [
4590
4626
  .../* @__PURE__ */ new Set([
4591
4627
  "agentrific-desktop://app",
4628
+ "https://app.agentrific.com",
4592
4629
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
4593
4630
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
4594
4631
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -5121,6 +5158,7 @@ var init_index = __esm({
5121
5158
  () => ({
5122
5159
  serverId: security.serverId,
5123
5160
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
5161
+ localUrl: localServerUrl(),
5124
5162
  serverVersion,
5125
5163
  connectedDevices: new Set(security.sockets.values()).size,
5126
5164
  connectedSockets: security.sockets.size,
@@ -5128,7 +5166,11 @@ var init_index = __esm({
5128
5166
  activeRuns: activeAgents(Effect17.runSync(store.list())),
5129
5167
  maintenance: maintenance.status()
5130
5168
  }),
5131
- maintenance
5169
+ maintenance,
5170
+ async () => {
5171
+ await app.close();
5172
+ process.exit(0);
5173
+ }
5132
5174
  );
5133
5175
  push.start();
5134
5176
  app.addHook("onClose", async () => {
@@ -5296,7 +5338,7 @@ function signalChild(child, signal) {
5296
5338
  }
5297
5339
  }
5298
5340
  function execute2(file, args, timeoutMs, env = process.env) {
5299
- return new Promise((resolve, reject) => {
5341
+ return new Promise((resolve2, reject) => {
5300
5342
  const child = spawn(file, args, {
5301
5343
  env,
5302
5344
  detached: process.platform !== "win32",
@@ -5324,7 +5366,7 @@ function execute2(file, args, timeoutMs, env = process.env) {
5324
5366
  child.once("close", (code) => {
5325
5367
  clearTimeout(timeout);
5326
5368
  if (code !== 0) reject(new Error("Command probe failed."));
5327
- else resolve({ stdout, stderr });
5369
+ else resolve2({ stdout, stderr });
5328
5370
  });
5329
5371
  function checkOutput() {
5330
5372
  if (stdout.length + stderr.length <= 1048576) return;
@@ -5471,7 +5513,7 @@ var ManagedTunnel = class {
5471
5513
  process.once("SIGINT", this.stopOnSignal);
5472
5514
  process.once("SIGTERM", this.stopOnSignal);
5473
5515
  process.once("exit", this.stopOnExit);
5474
- return new Promise((resolve, reject) => {
5516
+ return new Promise((resolve2, reject) => {
5475
5517
  let output = "";
5476
5518
  let address;
5477
5519
  let settled = false;
@@ -5499,7 +5541,7 @@ var ManagedTunnel = class {
5499
5541
  this.connected = true;
5500
5542
  clearTimeout(timeout);
5501
5543
  signal?.removeEventListener("abort", abort);
5502
- resolve(address);
5544
+ resolve2(address);
5503
5545
  };
5504
5546
  child.stdout?.on("data", read);
5505
5547
  child.stderr?.on("data", read);
@@ -5530,11 +5572,11 @@ var ManagedTunnel = class {
5530
5572
  const child = this.child;
5531
5573
  if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
5532
5574
  return Promise.resolve();
5533
- this.closing ??= new Promise((resolve) => {
5575
+ this.closing ??= new Promise((resolve2) => {
5534
5576
  const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
5535
5577
  child.once("close", () => {
5536
5578
  clearTimeout(timeout);
5537
- resolve();
5579
+ resolve2();
5538
5580
  });
5539
5581
  signalChild(child, "SIGTERM");
5540
5582
  });