@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/index.js CHANGED
@@ -1,27 +1,7 @@
1
- // src/warnings.ts
2
- var handlers = process.rawListeners("warning");
3
- process.removeAllListeners("warning");
4
- process.on("warning", (warning) => {
5
- if (warning.name === "ExperimentalWarning" && warning.message === "SQLite is an experimental feature and might change at any time")
6
- return;
7
- for (const handler of handlers) handler.call(process, warning);
8
- });
9
-
10
- // src/push.ts
11
- import {
12
- generateKeyPairSync,
13
- createPrivateKey,
14
- createPublicKey,
15
- sign,
16
- randomUUID
17
- } from "node:crypto";
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import path from "node:path";
20
- import { Effect } from "effect";
21
-
22
1
  // ../../packages/contracts/src/push.ts
23
2
  import { z } from "zod";
24
3
  var PUSH_API_URL = "https://api.agentrific.com";
4
+ var PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
25
5
  var PushKindSchema = z.enum([
26
6
  "started",
27
7
  "completed",
@@ -85,7 +65,26 @@ function pushSigningText(installationId, operation, request) {
85
65
  ].join("\n");
86
66
  }
87
67
 
68
+ // src/warnings.ts
69
+ var handlers = process.rawListeners("warning");
70
+ process.removeAllListeners("warning");
71
+ process.on("warning", (warning) => {
72
+ if (warning.name === "ExperimentalWarning" && warning.message === "SQLite is an experimental feature and might change at any time")
73
+ return;
74
+ for (const handler of handlers) handler.call(process, warning);
75
+ });
76
+
88
77
  // src/push.ts
78
+ import {
79
+ generateKeyPairSync,
80
+ createPrivateKey,
81
+ createPublicKey,
82
+ sign,
83
+ randomUUID
84
+ } from "node:crypto";
85
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
86
+ import path from "node:path";
87
+ import { Effect } from "effect";
89
88
  function pushKind(session, previous) {
90
89
  if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
91
90
  return "pr_created";
@@ -957,7 +956,7 @@ function attachEventServer(app2, security2, origins, snapshot) {
957
956
  }
958
957
 
959
958
  // src/security.ts
960
- import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
959
+ import { createHash as createHash2, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
961
960
  import { Effect as Effect4 } from "effect";
962
961
  import { z as z6 } from "zod";
963
962
 
@@ -967,10 +966,23 @@ import qrcode from "qrcode-terminal";
967
966
 
968
967
  // src/control.ts
969
968
  import { createServer, createConnection } from "node:net";
970
- import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
969
+ import { chmod, mkdir as mkdir2, lstat, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
971
970
  import path3 from "node:path";
972
971
  import { Effect as Effect3 } from "effect";
973
972
  import { z as z4 } from "zod";
973
+
974
+ // ../../packages/contracts/src/control-socket.ts
975
+ import { createHash } from "node:crypto";
976
+ import { join, resolve } from "node:path";
977
+ function controlSocketPath(directory, uid = process.getuid?.()) {
978
+ const direct = join(resolve(directory), "control.sock");
979
+ if (Buffer.byteLength(direct) < 100) return direct;
980
+ if (uid === void 0) throw new Error("Local control sockets require macOS or Linux.");
981
+ const digest2 = createHash("sha256").update(resolve(directory)).digest("hex").slice(0, 32);
982
+ return join("/tmp", `agentrific-${uid}`, `${digest2}.sock`);
983
+ }
984
+
985
+ // src/control.ts
974
986
  var MaintenanceSchema = z4.object({
975
987
  active: z4.boolean(),
976
988
  drained: z4.boolean(),
@@ -979,6 +991,7 @@ var MaintenanceSchema = z4.object({
979
991
  var ControlStatusSchema = z4.object({
980
992
  serverId: z4.string().uuid(),
981
993
  publicUrl: z4.string().optional(),
994
+ localUrl: z4.string().optional(),
982
995
  serverVersion: z4.string().optional(),
983
996
  connectedDevices: z4.number().optional(),
984
997
  connectedSockets: z4.number().optional(),
@@ -996,12 +1009,14 @@ var ControlResultSchema = z4.union([
996
1009
  DeviceSchema.array(),
997
1010
  ControlStatusSchema,
998
1011
  z4.object({ revoked: z4.boolean() }),
1012
+ z4.object({ shutdown: z4.literal(true) }),
999
1013
  MaintenanceSchema
1000
1014
  ]);
1001
1015
  var ControlRequest = z4.discriminatedUnion("command", [
1002
1016
  z4.object({ command: z4.literal("pair") }),
1003
1017
  z4.object({ command: z4.literal("devices") }),
1004
1018
  z4.object({ command: z4.literal("status") }),
1019
+ z4.object({ command: z4.literal("shutdown") }),
1005
1020
  z4.object({ command: z4.literal("revoke"), id: z4.string().uuid() }),
1006
1021
  z4.object({
1007
1022
  command: z4.literal("maintenance"),
@@ -1050,8 +1065,14 @@ async function acquireServerLock(directory) {
1050
1065
  await unlink(filename);
1051
1066
  };
1052
1067
  }
1053
- async function startControl(directory, security2, status, maintenance2) {
1054
- const filename = path3.join(directory, "control.sock");
1068
+ async function startControl(directory, security2, status, maintenance2, shutdown) {
1069
+ const filename = controlSocketPath(directory);
1070
+ const socketDirectory = path3.dirname(filename);
1071
+ await mkdir2(socketDirectory, { recursive: true, mode: 448 });
1072
+ const metadata = await lstat(socketDirectory);
1073
+ if (!metadata.isDirectory() || metadata.uid !== process.getuid?.())
1074
+ throw new Error("Local control directory must be private and owned by this user.");
1075
+ await chmod(socketDirectory, 448);
1055
1076
  await unlink(filename).catch((error) => {
1056
1077
  if (!z4.object({ code: z4.literal("ENOENT") }).safeParse(error).success) throw error;
1057
1078
  });
@@ -1071,6 +1092,13 @@ async function startControl(directory, security2, status, maintenance2) {
1071
1092
  const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
1072
1093
  let result;
1073
1094
  switch (request.command) {
1095
+ case "shutdown":
1096
+ if (!shutdown || !maintenance2?.status().drained)
1097
+ throw new Error("Drain the server before shutdown");
1098
+ socket.end(JSON.stringify({ result: { shutdown: true } }) + "\n", () => {
1099
+ setImmediate(() => void shutdown());
1100
+ });
1101
+ return;
1074
1102
  case "pair":
1075
1103
  result = security2.createPairing();
1076
1104
  break;
@@ -1097,14 +1125,14 @@ async function startControl(directory, security2, status, maintenance2) {
1097
1125
  });
1098
1126
  socket.on("error", () => socket.destroy());
1099
1127
  });
1100
- await new Promise((resolve, reject) => {
1128
+ await new Promise((resolve2, reject) => {
1101
1129
  server.once("error", reject);
1102
- server.listen(filename, resolve);
1130
+ server.listen(filename, resolve2);
1103
1131
  });
1104
1132
  await chmod(filename, 384);
1105
1133
  return async () => {
1106
1134
  await new Promise(
1107
- (resolve, reject) => server.close((error) => error ? reject(error) : resolve())
1135
+ (resolve2, reject) => server.close((error) => error ? reject(error) : resolve2())
1108
1136
  );
1109
1137
  };
1110
1138
  }
@@ -1112,7 +1140,7 @@ async function startControl(directory, security2, status, maintenance2) {
1112
1140
  // package.json
1113
1141
  var package_default = {
1114
1142
  name: "@agentrific/server",
1115
- version: "0.5.0",
1143
+ version: "0.5.2",
1116
1144
  description: "Self-hosted control plane for long-running coding agents",
1117
1145
  keywords: [
1118
1146
  "agentrific",
@@ -1188,7 +1216,7 @@ function pairingOrigin(url) {
1188
1216
 
1189
1217
  // src/security.ts
1190
1218
  var secret = () => randomBytes(32).toString("hex");
1191
- var digest = (value) => createHash("sha256").update(value).digest("hex");
1219
+ var digest = (value) => createHash2("sha256").update(value).digest("hex");
1192
1220
  var ServerSecurity = class {
1193
1221
  constructor(store2, serverId) {
1194
1222
  this.store = store2;
@@ -1386,7 +1414,7 @@ var Maintenance = class {
1386
1414
  this.enter();
1387
1415
  const deadline = Date.now() + timeoutMs;
1388
1416
  while (!this.status().drained && Date.now() < deadline)
1389
- await new Promise((resolve) => setTimeout(resolve, 50));
1417
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1390
1418
  return this.status();
1391
1419
  }
1392
1420
  attach(app2) {
@@ -3797,7 +3825,7 @@ var SessionStore = class extends Context3.Service()(
3797
3825
  };
3798
3826
 
3799
3827
  // src/worktree-cleanup.ts
3800
- import { lstat, rm as rm2 } from "node:fs/promises";
3828
+ import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
3801
3829
  import path8 from "node:path";
3802
3830
  import { Effect as Effect15 } from "effect";
3803
3831
  var WorktreeCleanup = class {
@@ -3812,7 +3840,7 @@ var WorktreeCleanup = class {
3812
3840
  async remove(session, discardChanges = true) {
3813
3841
  if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
3814
3842
  const workspace = path8.resolve(session.workspacePath);
3815
- const info = await lstat(workspace).catch((error) => {
3843
+ const info = await lstat2(workspace).catch((error) => {
3816
3844
  if (error.code === "ENOENT") return void 0;
3817
3845
  throw error;
3818
3846
  });
@@ -3924,13 +3952,23 @@ var releaseLock = await acquireServerLock(dataRoot);
3924
3952
  var effects = ManagedRuntime.make(SessionStore.layer(path9.join(dataRoot, "sessions.sqlite")));
3925
3953
  var store = await effects.runPromise(SessionStore);
3926
3954
  var security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
3955
+ var sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
3956
+ if (sandboxPushUrl && process.env.AGENTRIFIC_TEST_MODE !== "1")
3957
+ throw new Error("Sandbox push requires explicit test mode.");
3927
3958
  var testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
3928
3959
  if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
3929
3960
  throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
3930
- var push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
3961
+ var push = new ServerPush(
3962
+ store,
3963
+ security.serverId,
3964
+ dataRoot,
3965
+ fetch,
3966
+ testPushUrl ?? sandboxPushUrl
3967
+ );
3931
3968
  var allowedOrigins = [
3932
3969
  .../* @__PURE__ */ new Set([
3933
3970
  "agentrific-desktop://app",
3971
+ "https://app.agentrific.com",
3934
3972
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
3935
3973
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
3936
3974
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -4662,12 +4700,17 @@ attachEventServer(app, security, allowedOrigins, () => ({
4662
4700
  type: "snapshot",
4663
4701
  sessions: Effect17.runSync(store.list())
4664
4702
  }));
4703
+ function localServerUrl() {
4704
+ const address = z17.object({ port: z17.number() }).safeParse(app.server.address());
4705
+ return address.success ? `http://${host}:${address.data.port}` : void 0;
4706
+ }
4665
4707
  var closeControl = await startControl(
4666
4708
  dataRoot,
4667
4709
  security,
4668
4710
  () => ({
4669
4711
  serverId: security.serverId,
4670
4712
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
4713
+ localUrl: localServerUrl(),
4671
4714
  serverVersion,
4672
4715
  connectedDevices: new Set(security.sockets.values()).size,
4673
4716
  connectedSockets: security.sockets.size,
@@ -4675,7 +4718,11 @@ var closeControl = await startControl(
4675
4718
  activeRuns: activeAgents(Effect17.runSync(store.list())),
4676
4719
  maintenance: maintenance.status()
4677
4720
  }),
4678
- maintenance
4721
+ maintenance,
4722
+ async () => {
4723
+ await app.close();
4724
+ process.exit(0);
4725
+ }
4679
4726
  );
4680
4727
  push.start();
4681
4728
  app.addHook("onClose", async () => {