@agentrific/server 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
  # @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
+
11
+ ## 0.5.1
12
+
13
+ ### Patch Changes
14
+
15
+ - Add a separate sandbox push service and require explicit test mode for sandbox grants so TestFlight purchases cannot unlock production notifications.
16
+
3
17
  ## 0.5.0
4
18
 
5
19
  ### Minor 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.0",
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;
@@ -973,11 +1005,12 @@ function pushSigningText(installationId, operation, request) {
973
1005
  request.payload
974
1006
  ].join("\n");
975
1007
  }
976
- var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
1008
+ var PUSH_API_URL, PUSH_SANDBOX_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
977
1009
  var init_push = __esm({
978
1010
  "../../packages/contracts/src/push.ts"() {
979
1011
  "use strict";
980
1012
  PUSH_API_URL = "https://api.agentrific.com";
1013
+ PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
981
1014
  PushKindSchema = z6.enum([
982
1015
  "started",
983
1016
  "completed",
@@ -1541,7 +1574,7 @@ var init_event_server = __esm({
1541
1574
  });
1542
1575
 
1543
1576
  // src/security.ts
1544
- 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";
1545
1578
  import { Effect as Effect5 } from "effect";
1546
1579
  import { z as z8 } from "zod";
1547
1580
  async function registerSecurity(app2, security2, version, allowedOrigins2) {
@@ -1629,7 +1662,7 @@ var init_security = __esm({
1629
1662
  init_src();
1630
1663
  init_pairing();
1631
1664
  secret = () => randomBytes(32).toString("hex");
1632
- digest = (value) => createHash("sha256").update(value).digest("hex");
1665
+ digest = (value) => createHash2("sha256").update(value).digest("hex");
1633
1666
  ServerSecurity = class {
1634
1667
  constructor(store2, serverId) {
1635
1668
  this.store = store2;
@@ -1755,7 +1788,7 @@ var init_maintenance = __esm({
1755
1788
  this.enter();
1756
1789
  const deadline = Date.now() + timeoutMs;
1757
1790
  while (!this.status().drained && Date.now() < deadline)
1758
- await new Promise((resolve) => setTimeout(resolve, 50));
1791
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1759
1792
  return this.status();
1760
1793
  }
1761
1794
  attach(app2) {
@@ -4182,7 +4215,7 @@ var init_store = __esm({
4182
4215
  });
4183
4216
 
4184
4217
  // src/worktree-cleanup.ts
4185
- import { lstat, rm as rm2 } from "node:fs/promises";
4218
+ import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
4186
4219
  import path10 from "node:path";
4187
4220
  import { Effect as Effect15 } from "effect";
4188
4221
  async function removeRunFiles(dataRoot2, sessionId) {
@@ -4208,7 +4241,7 @@ var init_worktree_cleanup = __esm({
4208
4241
  async remove(session, discardChanges = true) {
4209
4242
  if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
4210
4243
  const workspace = path10.resolve(session.workspacePath);
4211
- const info = await lstat(workspace).catch((error) => {
4244
+ const info = await lstat2(workspace).catch((error) => {
4212
4245
  if (error.code === "ENOENT") return void 0;
4213
4246
  throw error;
4214
4247
  });
@@ -4512,10 +4545,15 @@ async function withSessionTaskLock(id, operation) {
4512
4545
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
4513
4546
  return withTaskLock(lockId, operation);
4514
4547
  }
4515
- var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
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
+ }
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;
4516
4553
  var init_index = __esm({
4517
4554
  async "src/index.ts"() {
4518
4555
  "use strict";
4556
+ init_push();
4519
4557
  init_warnings();
4520
4558
  init_push2();
4521
4559
  init_attachments();
@@ -4571,13 +4609,23 @@ var init_index = __esm({
4571
4609
  effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
4572
4610
  store = await effects.runPromise(SessionStore);
4573
4611
  security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
4612
+ sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
4613
+ if (sandboxPushUrl && process.env.AGENTRIFIC_TEST_MODE !== "1")
4614
+ throw new Error("Sandbox push requires explicit test mode.");
4574
4615
  testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
4575
4616
  if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
4576
4617
  throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
4577
- push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
4618
+ push = new ServerPush(
4619
+ store,
4620
+ security.serverId,
4621
+ dataRoot,
4622
+ fetch,
4623
+ testPushUrl ?? sandboxPushUrl
4624
+ );
4578
4625
  allowedOrigins = [
4579
4626
  .../* @__PURE__ */ new Set([
4580
4627
  "agentrific-desktop://app",
4628
+ "https://app.agentrific.com",
4581
4629
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
4582
4630
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
4583
4631
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -5110,6 +5158,7 @@ var init_index = __esm({
5110
5158
  () => ({
5111
5159
  serverId: security.serverId,
5112
5160
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
5161
+ localUrl: localServerUrl(),
5113
5162
  serverVersion,
5114
5163
  connectedDevices: new Set(security.sockets.values()).size,
5115
5164
  connectedSockets: security.sockets.size,
@@ -5117,7 +5166,11 @@ var init_index = __esm({
5117
5166
  activeRuns: activeAgents(Effect17.runSync(store.list())),
5118
5167
  maintenance: maintenance.status()
5119
5168
  }),
5120
- maintenance
5169
+ maintenance,
5170
+ async () => {
5171
+ await app.close();
5172
+ process.exit(0);
5173
+ }
5121
5174
  );
5122
5175
  push.start();
5123
5176
  app.addHook("onClose", async () => {
@@ -5285,7 +5338,7 @@ function signalChild(child, signal) {
5285
5338
  }
5286
5339
  }
5287
5340
  function execute2(file, args, timeoutMs, env = process.env) {
5288
- return new Promise((resolve, reject) => {
5341
+ return new Promise((resolve2, reject) => {
5289
5342
  const child = spawn(file, args, {
5290
5343
  env,
5291
5344
  detached: process.platform !== "win32",
@@ -5313,7 +5366,7 @@ function execute2(file, args, timeoutMs, env = process.env) {
5313
5366
  child.once("close", (code) => {
5314
5367
  clearTimeout(timeout);
5315
5368
  if (code !== 0) reject(new Error("Command probe failed."));
5316
- else resolve({ stdout, stderr });
5369
+ else resolve2({ stdout, stderr });
5317
5370
  });
5318
5371
  function checkOutput() {
5319
5372
  if (stdout.length + stderr.length <= 1048576) return;
@@ -5460,7 +5513,7 @@ var ManagedTunnel = class {
5460
5513
  process.once("SIGINT", this.stopOnSignal);
5461
5514
  process.once("SIGTERM", this.stopOnSignal);
5462
5515
  process.once("exit", this.stopOnExit);
5463
- return new Promise((resolve, reject) => {
5516
+ return new Promise((resolve2, reject) => {
5464
5517
  let output = "";
5465
5518
  let address;
5466
5519
  let settled = false;
@@ -5488,7 +5541,7 @@ var ManagedTunnel = class {
5488
5541
  this.connected = true;
5489
5542
  clearTimeout(timeout);
5490
5543
  signal?.removeEventListener("abort", abort);
5491
- resolve(address);
5544
+ resolve2(address);
5492
5545
  };
5493
5546
  child.stdout?.on("data", read);
5494
5547
  child.stderr?.on("data", read);
@@ -5519,11 +5572,11 @@ var ManagedTunnel = class {
5519
5572
  const child = this.child;
5520
5573
  if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
5521
5574
  return Promise.resolve();
5522
- this.closing ??= new Promise((resolve) => {
5575
+ this.closing ??= new Promise((resolve2) => {
5523
5576
  const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
5524
5577
  child.once("close", () => {
5525
5578
  clearTimeout(timeout);
5526
- resolve();
5579
+ resolve2();
5527
5580
  });
5528
5581
  signalChild(child, "SIGTERM");
5529
5582
  });