@agentrific/server 0.1.5 → 0.1.6

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
@@ -45,7 +45,14 @@ var PushGrantSchema = PushEnrollmentSchema.extend({
45
45
  grant: z.string().regex(/^[a-f0-9]{64}$/),
46
46
  expiresAt: z.number()
47
47
  }).strict();
48
- var PushEventSchema = z.object({ eventId: z.string().uuid(), sessionId: z.string().uuid(), kind: PushKindSchema }).strict();
48
+ var PushPullRequestUrlSchema = z.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
49
+ var PushEventSchema = z.object({
50
+ eventId: z.string().uuid(),
51
+ sessionId: z.string().uuid(),
52
+ kind: PushKindSchema,
53
+ taskTitle: z.string().trim().min(1).max(160).optional(),
54
+ prUrl: PushPullRequestUrlSchema.optional()
55
+ }).strict();
49
56
  var PushSignedRequestSchema = z.object({
50
57
  bindingId: z.string().uuid(),
51
58
  timestamp: z.number().int(),
@@ -105,10 +112,11 @@ function pushKind(session, previous) {
105
112
  }
106
113
  }
107
114
  var ServerPush = class {
108
- constructor(store2, serverId, directory, transport = fetch) {
115
+ constructor(store2, serverId, directory, transport = fetch, apiUrl = PUSH_API_URL) {
109
116
  this.store = store2;
110
117
  this.serverId = serverId;
111
118
  this.transport = transport;
119
+ this.apiUrl = apiUrl;
112
120
  mkdirSync(directory, { recursive: true, mode: 448 });
113
121
  const filename = path.join(directory, "push-signing-key.pem");
114
122
  if (!existsSync(filename))
@@ -123,6 +131,7 @@ var ServerPush = class {
123
131
  store;
124
132
  serverId;
125
133
  transport;
134
+ apiUrl;
126
135
  key;
127
136
  publicKey;
128
137
  draining;
@@ -136,6 +145,8 @@ var ServerPush = class {
136
145
  eventId: randomUUID(),
137
146
  sessionId: session.id,
138
147
  kind,
148
+ taskTitle: session.title.replace(/[\r\n\t]+/g, " ").trim().slice(0, 160) || void 0,
149
+ prUrl: kind === "pr_created" ? PushPullRequestUrlSchema.safeParse(session.gitDelivery?.url).data : void 0,
139
150
  remaining: state.bindings,
140
151
  createdAt: Date.now(),
141
152
  attempts: 0,
@@ -158,7 +169,7 @@ var ServerPush = class {
158
169
  Buffer.from(pushSigningText(installationId, operation, request)),
159
170
  this.key
160
171
  ).toString("hex");
161
- return this.transport(`${PUSH_API_URL}/v1/installations/${installationId}/${operation}`, {
172
+ return this.transport(`${this.apiUrl}/v1/installations/${installationId}/${operation}`, {
162
173
  method: "POST",
163
174
  headers: { "Content-Type": "application/json" },
164
175
  body: JSON.stringify({ ...request, signature }),
@@ -232,7 +243,13 @@ var ServerPush = class {
232
243
  binding.installationId,
233
244
  binding.bindingId,
234
245
  "events",
235
- JSON.stringify({ eventId: job.eventId, sessionId: job.sessionId, kind: job.kind })
246
+ JSON.stringify({
247
+ eventId: job.eventId,
248
+ sessionId: job.sessionId,
249
+ kind: job.kind,
250
+ taskTitle: job.taskTitle,
251
+ prUrl: job.prUrl
252
+ })
236
253
  );
237
254
  if (response.ok) continue;
238
255
  if ([401, 403, 404].includes(response.status)) {
@@ -613,6 +630,11 @@ var PairingPayloadSchema = z2.object({
613
630
  serverId: z2.string().uuid(),
614
631
  pairingId: z2.string().regex(/^[a-f0-9]{64}$/)
615
632
  });
633
+ var PairingInvitationSchema = z2.object({
634
+ payload: PairingPayloadSchema,
635
+ pin: z2.string().regex(/^\d{6}$/),
636
+ expiresAt: z2.number()
637
+ });
616
638
  var PairingRequestSchema = z2.object({
617
639
  pairingId: z2.string().regex(/^[a-f0-9]{64}$/),
618
640
  pin: z2.string().regex(/^\d{6}$/),
@@ -905,190 +927,55 @@ function attachEventServer(app2, security2, origins, snapshot) {
905
927
 
906
928
  // src/security.ts
907
929
  import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
908
- import { Effect as Effect3 } from "effect";
909
- import { z as z4 } from "zod";
910
- var secret = () => randomBytes(32).toString("hex");
911
- var digest = (value) => createHash("sha256").update(value).digest("hex");
912
- var ServerSecurity = class {
913
- constructor(store2, serverId) {
914
- this.store = store2;
915
- this.serverId = serverId;
916
- }
917
- store;
918
- serverId;
919
- grants = /* @__PURE__ */ new Map();
920
- tickets = /* @__PURE__ */ new Map();
921
- attempts = /* @__PURE__ */ new Map();
922
- sockets = /* @__PURE__ */ new Map();
923
- prune() {
924
- const now = Date.now();
925
- for (const [key, grant] of this.grants) if (grant.expiresAt <= now) this.grants.delete(key);
926
- for (const [key, ticket] of this.tickets) if (ticket.expiresAt <= now) this.tickets.delete(key);
927
- for (const [key, attempts] of this.attempts)
928
- if (attempts.until <= now) this.attempts.delete(key);
929
- }
930
- createPairing() {
931
- this.prune();
932
- if (this.grants.size >= 10)
933
- throw new Error("Too many pending enrollments. Wait for them to expire.");
934
- const pairingId = secret();
935
- const grant = {
936
- pin: String(randomInt(1e6)).padStart(6, "0"),
937
- expiresAt: Date.now() + 3e5,
938
- attempts: 0
939
- };
940
- this.grants.set(pairingId, grant);
941
- return { pairingId, pin: grant.pin, expiresAt: grant.expiresAt, serverId: this.serverId };
942
- }
943
- async pair(input, address) {
944
- this.prune();
945
- const attempts = this.attempts.get(address) ?? { count: 0, until: Date.now() + 6e4 };
946
- if (attempts.count >= 20 || !this.attempts.has(address) && this.attempts.size >= 5e3)
947
- return void 0;
948
- attempts.count++;
949
- this.attempts.set(address, attempts);
950
- const grant = this.grants.get(input.pairingId);
951
- if (!grant) return void 0;
952
- grant.attempts++;
953
- const match = timingSafeEqual(Buffer.from(grant.pin), Buffer.from(input.pin));
954
- if (grant.attempts >= 3 || match) this.grants.delete(input.pairingId);
955
- if (!match) return void 0;
956
- const token = secret();
957
- const now = Date.now();
958
- const device = {
959
- id: randomUUID3(),
960
- name: input.deviceName,
961
- platform: input.platform,
962
- createdAt: now,
963
- lastSeenAt: now,
964
- revokedAt: null
965
- };
966
- await Effect3.runPromise(this.store.saveDevice(device, digest(token)));
967
- return { deviceId: device.id, deviceToken: token, serverId: this.serverId };
968
- }
969
- authenticate(token) {
970
- if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
971
- return Effect3.runSync(this.store.deviceForToken(digest(token)));
972
- }
973
- ticket(token, path10) {
974
- this.prune();
975
- if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
976
- const value = secret();
977
- this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path10 });
978
- return value;
979
- }
980
- redeemTicket(value, path10, consume = true) {
981
- this.prune();
982
- const ticket = this.tickets.get(value);
983
- if (!ticket || ticket.path !== path10) return void 0;
984
- if (consume) this.tickets.delete(value);
985
- return Effect3.runSync(this.store.deviceForToken(ticket.tokenHash));
986
- }
987
- async revoke(id) {
988
- const revoked = await Effect3.runPromise(this.store.revokeDevice(id));
989
- for (const [socket, owner] of this.sockets)
990
- if (owner === id) socket.close(4401, "Device revoked");
991
- return revoked;
992
- }
993
- };
994
- async function registerSecurity(app2, security2, version, allowedOrigins2) {
995
- app2.addHook("onRequest", async (request, reply) => {
996
- const path10 = new URL(request.url, "http://localhost");
997
- reply.header("Cache-Control", "no-store");
998
- if (request.method === "OPTIONS") return;
999
- if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
1000
- return reply.code(403).send({ error: "Browser origin is not allowed." });
1001
- if (path10.pathname === "/health" || path10.pathname === "/api/auth/pair" && request.method === "POST")
1002
- return;
1003
- const authorization = request.headers.authorization ?? "";
1004
- const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
1005
- const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path10.pathname);
1006
- const ticket = path10.searchParams.get("ticket");
1007
- path10.searchParams.delete("ticket");
1008
- if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path10.pathname + path10.search, false)))
1009
- return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
1010
- if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
1011
- return reply.code(409).send({
1012
- error: "Update your Agentrific client or server.",
1013
- code: "VERSION_MISMATCH",
1014
- protocolVersion: PROTOCOL_VERSION
1015
- });
1016
- });
1017
- app2.get("/health", () => ({
1018
- status: "ok",
1019
- service: "agentrific-server",
1020
- serverId: security2.serverId,
1021
- serverVersion: version,
1022
- protocolVersion: PROTOCOL_VERSION,
1023
- authentication: "pairing"
1024
- }));
1025
- app2.post("/api/auth/pair", async (request, reply) => {
1026
- const input = PairingRequestSchema.safeParse(request.body);
1027
- if (!input.success)
1028
- return reply.code(400).send({ error: "Enter a valid pairing payload and six-digit PIN." });
1029
- const result = await security2.pair(input.data, request.ip);
1030
- return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
1031
- });
1032
- app2.get("/api/auth/session", (request) => ({
1033
- serverId: security2.serverId,
1034
- device: security2.authenticate((request.headers.authorization ?? "").slice(7))
1035
- }));
1036
- app2.post("/api/auth/event-ticket", (request) => ({
1037
- ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
1038
- }));
1039
- app2.post("/api/auth/asset-ticket", (request, reply) => {
1040
- const input = z4.object({
1041
- path: z4.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
1042
- }).safeParse(request.body);
1043
- if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
1044
- return {
1045
- ticket: security2.ticket((request.headers.authorization ?? "").slice(7), input.data.path)
1046
- };
1047
- });
1048
- }
930
+ import { Effect as Effect4 } from "effect";
931
+ import { z as z6 } from "zod";
932
+
933
+ // src/pairing.ts
934
+ import { z as z5 } from "zod";
935
+ import qrcode from "qrcode-terminal";
1049
936
 
1050
937
  // src/control.ts
1051
938
  import { createServer, createConnection } from "node:net";
1052
939
  import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
1053
940
  import path3 from "node:path";
1054
- import { Effect as Effect4 } from "effect";
1055
- import { z as z5 } from "zod";
1056
- var MaintenanceSchema = z5.object({
1057
- active: z5.boolean(),
1058
- drained: z5.boolean(),
1059
- blockers: z5.array(z5.string())
941
+ import { Effect as Effect3 } from "effect";
942
+ import { z as z4 } from "zod";
943
+ var MaintenanceSchema = z4.object({
944
+ active: z4.boolean(),
945
+ drained: z4.boolean(),
946
+ blockers: z4.array(z4.string())
1060
947
  });
1061
- var ControlStatusSchema = z5.object({
1062
- serverId: z5.string().uuid(),
1063
- publicUrl: z5.string().optional(),
1064
- serverVersion: z5.string().optional(),
1065
- connectedDevices: z5.number().optional(),
1066
- connectedSockets: z5.number().optional(),
1067
- memory: z5.number().optional(),
1068
- activeRuns: z5.number().optional(),
948
+ var ControlStatusSchema = z4.object({
949
+ serverId: z4.string().uuid(),
950
+ publicUrl: z4.string().optional(),
951
+ serverVersion: z4.string().optional(),
952
+ connectedDevices: z4.number().optional(),
953
+ connectedSockets: z4.number().optional(),
954
+ memory: z4.number().optional(),
955
+ activeRuns: z4.number().optional(),
1069
956
  maintenance: MaintenanceSchema.optional()
1070
957
  });
1071
- var ControlResultSchema = z5.union([
1072
- z5.object({
1073
- pairingId: z5.string(),
1074
- pin: z5.string(),
1075
- expiresAt: z5.number(),
1076
- serverId: z5.string().uuid()
958
+ var ControlResultSchema = z4.union([
959
+ z4.object({
960
+ pairingId: z4.string(),
961
+ pin: z4.string(),
962
+ expiresAt: z4.number(),
963
+ serverId: z4.string().uuid()
1077
964
  }),
1078
965
  DeviceSchema.array(),
1079
966
  ControlStatusSchema,
1080
- z5.object({ revoked: z5.boolean() }),
967
+ z4.object({ revoked: z4.boolean() }),
1081
968
  MaintenanceSchema
1082
969
  ]);
1083
- var ControlRequest = z5.discriminatedUnion("command", [
1084
- z5.object({ command: z5.literal("pair") }),
1085
- z5.object({ command: z5.literal("devices") }),
1086
- z5.object({ command: z5.literal("status") }),
1087
- z5.object({ command: z5.literal("revoke"), id: z5.string().uuid() }),
1088
- z5.object({
1089
- command: z5.literal("maintenance"),
1090
- action: z5.enum(["enter", "resume", "drain"]),
1091
- timeoutMs: z5.number().int().min(0).max(6e4).default(3e4)
970
+ var ControlRequest = z4.discriminatedUnion("command", [
971
+ z4.object({ command: z4.literal("pair") }),
972
+ z4.object({ command: z4.literal("devices") }),
973
+ z4.object({ command: z4.literal("status") }),
974
+ z4.object({ command: z4.literal("revoke"), id: z4.string().uuid() }),
975
+ z4.object({
976
+ command: z4.literal("maintenance"),
977
+ action: z4.enum(["enter", "resume", "drain"]),
978
+ timeoutMs: z4.number().int().min(0).max(6e4).default(3e4)
1092
979
  })
1093
980
  ]);
1094
981
  async function acquireServerLock(directory) {
@@ -1098,14 +985,14 @@ async function acquireServerLock(directory) {
1098
985
  try {
1099
986
  await writeFile(filename, String(process.pid), { flag: "wx", mode: 384 });
1100
987
  } catch (cause) {
1101
- if (!z5.object({ code: z5.literal("EEXIST") }).safeParse(cause).success) throw cause;
988
+ if (!z4.object({ code: z4.literal("EEXIST") }).safeParse(cause).success) throw cause;
1102
989
  const owner = Number(await readFile2(filename, "utf8"));
1103
990
  if (!Number.isInteger(owner) || owner <= 0)
1104
991
  throw new Error("Invalid server lock. Check the owner before removing it.");
1105
992
  try {
1106
993
  process.kill(owner, 0);
1107
994
  } catch (error) {
1108
- if (!z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) throw error;
995
+ if (!z4.object({ code: z4.literal("ESRCH") }).safeParse(error).success) throw error;
1109
996
  const recovery = `${filename}.recovery`;
1110
997
  await writeFile(recovery, String(process.pid), { flag: "wx", mode: 384 }).catch(() => {
1111
998
  throw new Error(
@@ -1135,7 +1022,7 @@ async function acquireServerLock(directory) {
1135
1022
  async function startControl(directory, security2, status, maintenance2) {
1136
1023
  const filename = path3.join(directory, "control.sock");
1137
1024
  await unlink(filename).catch((error) => {
1138
- if (!z5.object({ code: z5.literal("ENOENT") }).safeParse(error).success) throw error;
1025
+ if (!z4.object({ code: z4.literal("ENOENT") }).safeParse(error).success) throw error;
1139
1026
  });
1140
1027
  const server = createServer((socket) => {
1141
1028
  socket.setTimeout(5e3, () => socket.destroy());
@@ -1157,7 +1044,7 @@ async function startControl(directory, security2, status, maintenance2) {
1157
1044
  result = security2.createPairing();
1158
1045
  break;
1159
1046
  case "devices":
1160
- result = await Effect4.runPromise(security2.store.devices());
1047
+ result = await Effect3.runPromise(security2.store.devices());
1161
1048
  break;
1162
1049
  case "status":
1163
1050
  result = status();
@@ -1194,7 +1081,7 @@ async function startControl(directory, security2, status, maintenance2) {
1194
1081
  // package.json
1195
1082
  var package_default = {
1196
1083
  name: "@agentrific/server",
1197
- version: "0.1.5",
1084
+ version: "0.1.6",
1198
1085
  description: "Self-hosted control plane for long-running coding agents",
1199
1086
  keywords: [
1200
1087
  "agentrific",
@@ -1257,6 +1144,180 @@ var package_default = {
1257
1144
  // src/version.ts
1258
1145
  var serverVersion = package_default.version;
1259
1146
 
1147
+ // src/pairing.ts
1148
+ function pairingOrigin(url) {
1149
+ const endpoint = new URL(url);
1150
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
1151
+ throw new Error("Use the HTTPS server origin without a path or credentials.");
1152
+ if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
1153
+ throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
1154
+ return endpoint.origin;
1155
+ }
1156
+
1157
+ // src/security.ts
1158
+ var secret = () => randomBytes(32).toString("hex");
1159
+ var digest = (value) => createHash("sha256").update(value).digest("hex");
1160
+ var ServerSecurity = class {
1161
+ constructor(store2, serverId) {
1162
+ this.store = store2;
1163
+ this.serverId = serverId;
1164
+ }
1165
+ store;
1166
+ serverId;
1167
+ grants = /* @__PURE__ */ new Map();
1168
+ tickets = /* @__PURE__ */ new Map();
1169
+ attempts = /* @__PURE__ */ new Map();
1170
+ sockets = /* @__PURE__ */ new Map();
1171
+ prune() {
1172
+ const now = Date.now();
1173
+ for (const [key, grant] of this.grants) if (grant.expiresAt <= now) this.grants.delete(key);
1174
+ for (const [key, ticket] of this.tickets) if (ticket.expiresAt <= now) this.tickets.delete(key);
1175
+ for (const [key, attempts] of this.attempts)
1176
+ if (attempts.until <= now) this.attempts.delete(key);
1177
+ }
1178
+ createPairing() {
1179
+ this.prune();
1180
+ if (this.grants.size >= 10)
1181
+ throw new Error("Too many pending enrollments. Wait for them to expire.");
1182
+ const pairingId = secret();
1183
+ const grant = {
1184
+ pin: String(randomInt(1e6)).padStart(6, "0"),
1185
+ expiresAt: Date.now() + 3e5,
1186
+ attempts: 0
1187
+ };
1188
+ this.grants.set(pairingId, grant);
1189
+ return { pairingId, pin: grant.pin, expiresAt: grant.expiresAt, serverId: this.serverId };
1190
+ }
1191
+ async pair(input, address) {
1192
+ this.prune();
1193
+ const attempts = this.attempts.get(address) ?? { count: 0, until: Date.now() + 6e4 };
1194
+ if (attempts.count >= 20 || !this.attempts.has(address) && this.attempts.size >= 5e3)
1195
+ return void 0;
1196
+ attempts.count++;
1197
+ this.attempts.set(address, attempts);
1198
+ const grant = this.grants.get(input.pairingId);
1199
+ if (!grant) return void 0;
1200
+ grant.attempts++;
1201
+ const match = timingSafeEqual(Buffer.from(grant.pin), Buffer.from(input.pin));
1202
+ if (grant.attempts >= 3 || match) this.grants.delete(input.pairingId);
1203
+ if (!match) return void 0;
1204
+ const token = secret();
1205
+ const now = Date.now();
1206
+ const device = {
1207
+ id: randomUUID3(),
1208
+ name: input.deviceName,
1209
+ platform: input.platform,
1210
+ createdAt: now,
1211
+ lastSeenAt: now,
1212
+ revokedAt: null
1213
+ };
1214
+ await Effect4.runPromise(this.store.saveDevice(device, digest(token)));
1215
+ return { deviceId: device.id, deviceToken: token, serverId: this.serverId };
1216
+ }
1217
+ authenticate(token) {
1218
+ if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
1219
+ return Effect4.runSync(this.store.deviceForToken(digest(token)));
1220
+ }
1221
+ ticket(token, path10) {
1222
+ this.prune();
1223
+ if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
1224
+ const value = secret();
1225
+ this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path10 });
1226
+ return value;
1227
+ }
1228
+ redeemTicket(value, path10, consume = true) {
1229
+ this.prune();
1230
+ const ticket = this.tickets.get(value);
1231
+ if (!ticket || ticket.path !== path10) return void 0;
1232
+ if (consume) this.tickets.delete(value);
1233
+ return Effect4.runSync(this.store.deviceForToken(ticket.tokenHash));
1234
+ }
1235
+ async revoke(id) {
1236
+ const revoked = await Effect4.runPromise(this.store.revokeDevice(id));
1237
+ for (const [socket, owner] of this.sockets)
1238
+ if (owner === id) socket.close(4401, "Device revoked");
1239
+ return revoked;
1240
+ }
1241
+ };
1242
+ async function registerSecurity(app2, security2, version, allowedOrigins2) {
1243
+ app2.addHook("onRequest", async (request, reply) => {
1244
+ const path10 = new URL(request.url, "http://localhost");
1245
+ reply.header("Cache-Control", "no-store");
1246
+ if (request.method === "OPTIONS") return;
1247
+ if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
1248
+ return reply.code(403).send({ error: "Browser origin is not allowed." });
1249
+ if (path10.pathname === "/health" || path10.pathname === "/api/auth/pair" && request.method === "POST")
1250
+ return;
1251
+ const authorization = request.headers.authorization ?? "";
1252
+ const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
1253
+ const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path10.pathname);
1254
+ const ticket = path10.searchParams.get("ticket");
1255
+ path10.searchParams.delete("ticket");
1256
+ if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path10.pathname + path10.search, false)))
1257
+ return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
1258
+ if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
1259
+ return reply.code(409).send({
1260
+ error: "Update your Agentrific client or server.",
1261
+ code: "VERSION_MISMATCH",
1262
+ protocolVersion: PROTOCOL_VERSION
1263
+ });
1264
+ });
1265
+ app2.get("/health", () => ({
1266
+ status: "ok",
1267
+ service: "agentrific-server",
1268
+ serverId: security2.serverId,
1269
+ serverVersion: version,
1270
+ protocolVersion: PROTOCOL_VERSION,
1271
+ authentication: "pairing"
1272
+ }));
1273
+ app2.post("/api/auth/pair", async (request, reply) => {
1274
+ const input = PairingRequestSchema.safeParse(request.body);
1275
+ if (!input.success)
1276
+ return reply.code(400).send({ error: "Enter a valid pairing payload and six-digit PIN." });
1277
+ const result = await security2.pair(input.data, request.ip);
1278
+ return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
1279
+ });
1280
+ app2.post("/api/auth/pairing-invitation", (_request, reply) => {
1281
+ const publicUrl = process.env.AGENTRIFIC_PUBLIC_URL;
1282
+ if (!publicUrl)
1283
+ return reply.code(503).send({
1284
+ error: "Configure the server\u2019s public HTTPS URL and restart the server before pairing a device."
1285
+ });
1286
+ let url;
1287
+ try {
1288
+ url = pairingOrigin(publicUrl);
1289
+ } catch {
1290
+ return reply.code(503).send({ error: "The server\u2019s public URL is invalid. Configure a public HTTPS origin." });
1291
+ }
1292
+ try {
1293
+ const grant = security2.createPairing();
1294
+ return {
1295
+ payload: { v: 1, url, serverId: grant.serverId, pairingId: grant.pairingId },
1296
+ pin: grant.pin,
1297
+ expiresAt: grant.expiresAt
1298
+ };
1299
+ } catch {
1300
+ return reply.code(429).send({ error: "Too many pending pairing codes. Wait five minutes and try again." });
1301
+ }
1302
+ });
1303
+ app2.get("/api/auth/session", (request) => ({
1304
+ serverId: security2.serverId,
1305
+ device: security2.authenticate((request.headers.authorization ?? "").slice(7))
1306
+ }));
1307
+ app2.post("/api/auth/event-ticket", (request) => ({
1308
+ ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
1309
+ }));
1310
+ app2.post("/api/auth/asset-ticket", (request, reply) => {
1311
+ const input = z6.object({
1312
+ path: z6.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
1313
+ }).safeParse(request.body);
1314
+ if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
1315
+ return {
1316
+ ticket: security2.ticket((request.headers.authorization ?? "").slice(7), input.data.path)
1317
+ };
1318
+ });
1319
+ }
1320
+
1260
1321
  // src/maintenance.ts
1261
1322
  var MaintenanceError = class extends Error {
1262
1323
  statusCode = 503;
@@ -1313,10 +1374,10 @@ var Maintenance = class {
1313
1374
 
1314
1375
  // src/updates.ts
1315
1376
  import semver from "semver";
1316
- import { z as z6 } from "zod";
1377
+ import { z as z7 } from "zod";
1317
1378
  import { Effect as Effect5 } from "effect";
1318
- var RegistryRelease = z6.object({
1319
- version: z6.string().refine((value) => semver.valid(value) !== null)
1379
+ var RegistryRelease = z7.object({
1380
+ version: z7.string().refine((value) => semver.valid(value) !== null)
1320
1381
  });
1321
1382
  async function checkForUpdates() {
1322
1383
  const response = await fetch("https://registry.npmjs.org/@agentrific%2Fserver/latest", {
@@ -1406,7 +1467,7 @@ var UpdateDiscovery = class {
1406
1467
 
1407
1468
  // src/index.ts
1408
1469
  import { Effect as Effect16, ManagedRuntime } from "effect";
1409
- import { z as z15 } from "zod";
1470
+ import { z as z16 } from "zod";
1410
1471
 
1411
1472
  // src/automation.ts
1412
1473
  import { Effect as Effect7 } from "effect";
@@ -1911,7 +1972,7 @@ var AgentSessions = class extends Context2.Service()(
1911
1972
 
1912
1973
  // src/delivery.ts
1913
1974
  import { Effect as Effect9 } from "effect";
1914
- import { z as z8 } from "zod";
1975
+ import { z as z9 } from "zod";
1915
1976
 
1916
1977
  // src/workflow-command.ts
1917
1978
  import { execFile } from "node:child_process";
@@ -1955,12 +2016,12 @@ function workflowError(error) {
1955
2016
  }
1956
2017
 
1957
2018
  // src/workflow-schema.ts
1958
- import { z as z7 } from "zod";
1959
- var DeliveryJobSchema = z7.object({
1960
- kind: z7.literal("delivery"),
1961
- sessionId: z7.string(),
2019
+ import { z as z8 } from "zod";
2020
+ var DeliveryJobSchema = z8.object({
2021
+ kind: z8.literal("delivery"),
2022
+ sessionId: z8.string(),
1962
2023
  request: DeliveryRequestSchema,
1963
- phase: z7.enum([
2024
+ phase: z8.enum([
1964
2025
  "validating",
1965
2026
  "drafting",
1966
2027
  "committing",
@@ -1970,25 +2031,25 @@ var DeliveryJobSchema = z7.object({
1970
2031
  "opening_pr",
1971
2032
  "delivered"
1972
2033
  ]),
1973
- attempt: z7.number().int().positive(),
1974
- commitSha: z7.string().optional(),
1975
- title: z7.string(),
1976
- body: z7.string(),
1977
- logs: z7.string().default(""),
1978
- failed: z7.boolean().default(false),
1979
- result: z7.enum(["merged", "pr_opened", "no_changes"]).optional(),
1980
- url: z7.string().optional(),
1981
- message: z7.string().optional()
2034
+ attempt: z8.number().int().positive(),
2035
+ commitSha: z8.string().optional(),
2036
+ title: z8.string(),
2037
+ body: z8.string(),
2038
+ logs: z8.string().default(""),
2039
+ failed: z8.boolean().default(false),
2040
+ result: z8.enum(["merged", "pr_opened", "no_changes"]).optional(),
2041
+ url: z8.string().optional(),
2042
+ message: z8.string().optional()
1982
2043
  });
1983
- var PreparationJobSchema = z7.object({
1984
- kind: z7.literal("preparation"),
1985
- sessionId: z7.string(),
1986
- projectPath: z7.string(),
2044
+ var PreparationJobSchema = z8.object({
2045
+ kind: z8.literal("preparation"),
2046
+ sessionId: z8.string(),
2047
+ projectPath: z8.string(),
1987
2048
  input: CreateSessionSchema,
1988
- stage: z7.enum(["creating", "installing", "ready", "failed"]),
1989
- logs: z7.string().default("")
2049
+ stage: z8.enum(["creating", "installing", "ready", "failed"]),
2050
+ logs: z8.string().default("")
1990
2051
  });
1991
- var WorkflowJobSchema = z7.discriminatedUnion("kind", [
2052
+ var WorkflowJobSchema = z8.discriminatedUnion("kind", [
1992
2053
  DeliveryJobSchema,
1993
2054
  PreparationJobSchema
1994
2055
  ]);
@@ -2284,11 +2345,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
2284
2345
  "--json",
2285
2346
  "url,state,headRefOid"
2286
2347
  ]);
2287
- const prs = z8.array(
2288
- z8.object({
2289
- url: z8.string().url(),
2290
- state: z8.string(),
2291
- headRefOid: z8.string().optional()
2348
+ const prs = z9.array(
2349
+ z9.object({
2350
+ url: z9.string().url(),
2351
+ state: z9.string(),
2352
+ headRefOid: z9.string().optional()
2292
2353
  })
2293
2354
  ).parse(JSON.parse(existing));
2294
2355
  const open = prs.find(
@@ -2332,7 +2393,7 @@ ${workflowError(error instanceof Error ? error : String(error))}`
2332
2393
  import { mkdir as mkdir3, readFile as readFile3 } from "node:fs/promises";
2333
2394
  import path5 from "node:path";
2334
2395
  import { Effect as Effect10 } from "effect";
2335
- import { z as z9 } from "zod";
2396
+ import { z as z10 } from "zod";
2336
2397
  var PreparationCoordinator = class {
2337
2398
  constructor(store2, options) {
2338
2399
  this.store = store2;
@@ -2450,7 +2511,7 @@ ${logs}` : ""}`);
2450
2511
  () => void 0
2451
2512
  );
2452
2513
  if (!packageFile) return;
2453
- const metadata = z9.object({ packageManager: z9.string().optional() }).parse(JSON.parse(packageFile));
2514
+ const metadata = z10.object({ packageManager: z10.string().optional() }).parse(JSON.parse(packageFile));
2454
2515
  const has = async (name) => readFile3(path5.join(workspace, name)).then(
2455
2516
  () => true,
2456
2517
  () => false
@@ -2532,8 +2593,8 @@ ${logs}` : ""}`);
2532
2593
 
2533
2594
  // src/delivery-writer.ts
2534
2595
  import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
2535
- import { z as z10 } from "zod";
2536
- var DraftSchema = z10.object({ title: z10.string().trim().min(1).max(120), body: z10.string().trim().min(1).max(1e4) }).strict();
2596
+ import { z as z11 } from "zod";
2597
+ var DraftSchema = z11.object({ title: z11.string().trim().min(1).max(120), body: z11.string().trim().min(1).max(1e4) }).strict();
2537
2598
  function deliveryWriter(models2, createSession = createAgentSession) {
2538
2599
  return async (run, input, diff, signal) => {
2539
2600
  signal?.throwIfAborted();
@@ -2688,7 +2749,7 @@ import {
2688
2749
  createAgentSession as createAgentSession2,
2689
2750
  SessionManager as SessionManager2
2690
2751
  } from "@earendil-works/pi-coding-agent";
2691
- import { z as z11 } from "zod";
2752
+ import { z as z12 } from "zod";
2692
2753
 
2693
2754
  // src/delivery-tool.ts
2694
2755
  import { defineTool } from "@earendil-works/pi-coding-agent";
@@ -2723,8 +2784,8 @@ function deliveryTool(sessionId, delivery2) {
2723
2784
  }
2724
2785
 
2725
2786
  // src/pi.ts
2726
- var ToolResultSchema = z11.object({
2727
- content: z11.array(z11.object({ type: z11.string(), text: z11.string().optional() })).optional()
2787
+ var ToolResultSchema = z12.object({
2788
+ content: z12.array(z12.object({ type: z12.string(), text: z12.string().optional() })).optional()
2728
2789
  }).passthrough();
2729
2790
  var boundedJson = (value, limit) => {
2730
2791
  let text;
@@ -2890,7 +2951,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
2890
2951
  // src/projects.ts
2891
2952
  import { access, readdir as readdir2, realpath, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
2892
2953
  import { randomUUID as randomUUID6 } from "node:crypto";
2893
- import { z as z12 } from "zod";
2954
+ import { z as z13 } from "zod";
2894
2955
  import { constants } from "node:fs";
2895
2956
  import { homedir } from "node:os";
2896
2957
  import path6 from "node:path";
@@ -2922,7 +2983,7 @@ function projectLock(store2) {
2922
2983
  }
2923
2984
  return lock;
2924
2985
  }
2925
- var ProjectId = z12.string().trim().min(1).max(255);
2986
+ var ProjectId = z13.string().trim().min(1).max(255);
2926
2987
  async function gitDirectory(directory) {
2927
2988
  const location = path6.join(directory, ".git");
2928
2989
  const info = await stat2(location);
@@ -2939,10 +3000,10 @@ async function identity(directory, preferred) {
2939
3000
  try {
2940
3001
  return ProjectId.parse(await readFile4(filename, "utf8"));
2941
3002
  } catch (error) {
2942
- if (!z12.object({ code: z12.literal("ENOENT") }).safeParse(error).success) throw error;
3003
+ if (!z13.object({ code: z13.literal("ENOENT") }).safeParse(error).success) throw error;
2943
3004
  const legacyFilename = path6.join(await gitDirectory(directory), "chatplans-project-id");
2944
3005
  const legacy = await readFile4(legacyFilename, "utf8").catch((cause) => {
2945
- if (!z12.object({ code: z12.literal("ENOENT") }).safeParse(cause).success) throw cause;
3006
+ if (!z13.object({ code: z13.literal("ENOENT") }).safeParse(cause).success) throw cause;
2946
3007
  return null;
2947
3008
  });
2948
3009
  const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
@@ -2950,7 +3011,7 @@ async function identity(directory, preferred) {
2950
3011
  await writeFile2(filename, id + "\n", { flag: "wx" });
2951
3012
  return id;
2952
3013
  } catch (writeError) {
2953
- if (!z12.object({ code: z12.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3014
+ if (!z13.object({ code: z13.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
2954
3015
  return ProjectId.parse(await readFile4(filename, "utf8"));
2955
3016
  }
2956
3017
  }
@@ -3028,32 +3089,32 @@ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "n
3028
3089
  import path7 from "node:path";
3029
3090
 
3030
3091
  // src/push-schema.ts
3031
- import { z as z13 } from "zod";
3032
- var PushBindingSchema = z13.object({
3033
- bindingId: z13.string().uuid(),
3034
- installationId: z13.string().uuid(),
3035
- deviceId: z13.string().uuid()
3092
+ import { z as z14 } from "zod";
3093
+ var PushBindingSchema = z14.object({
3094
+ bindingId: z14.string().uuid(),
3095
+ installationId: z14.string().uuid(),
3096
+ deviceId: z14.string().uuid()
3036
3097
  });
3037
- var PushStateSchema = z13.object({
3038
- bindings: z13.array(PushBindingSchema).max(10),
3039
- error: z13.string().nullable()
3098
+ var PushStateSchema = z14.object({
3099
+ bindings: z14.array(PushBindingSchema).max(10),
3100
+ error: z14.string().nullable()
3040
3101
  });
3041
3102
  var PendingPushSchema = PushEventSchema.extend({
3042
- remaining: z13.array(PushBindingSchema),
3043
- createdAt: z13.number(),
3044
- attempts: z13.number().int().nonnegative(),
3045
- nextAt: z13.number()
3103
+ remaining: z14.array(PushBindingSchema),
3104
+ createdAt: z14.number(),
3105
+ attempts: z14.number().int().nonnegative(),
3106
+ nextAt: z14.number()
3046
3107
  });
3047
3108
 
3048
3109
  // src/store.ts
3049
3110
  import { DatabaseSync } from "node:sqlite";
3050
3111
  import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
3051
- import { z as z14 } from "zod";
3112
+ import { z as z15 } from "zod";
3052
3113
  import { randomUUID as randomUUID7 } from "node:crypto";
3053
3114
  var StorageError = class extends Data3.TaggedError("StorageError") {
3054
3115
  };
3055
- var PayloadRow = z14.object({ payload: z14.string() });
3056
- var EventRow = z14.object({ sequence: z14.number(), recorded_at: z14.string(), payload: z14.string() });
3116
+ var PayloadRow = z15.object({ payload: z15.string() });
3117
+ var EventRow = z15.object({ sequence: z15.number(), recorded_at: z15.string(), payload: z15.string() });
3057
3118
  var SqliteSessionStore = class {
3058
3119
  constructor(db) {
3059
3120
  this.db = db;
@@ -3104,7 +3165,7 @@ var SqliteSessionStore = class {
3104
3165
  const row = PayloadRow.parse(
3105
3166
  this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
3106
3167
  );
3107
- return z14.string().uuid().parse(JSON.parse(row.payload));
3168
+ return z15.string().uuid().parse(JSON.parse(row.payload));
3108
3169
  });
3109
3170
  }
3110
3171
  saveDevice(device, tokenHash) {
@@ -3192,7 +3253,7 @@ var SqliteSessionStore = class {
3192
3253
  `).all();
3193
3254
  const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
3194
3255
  for (const row of legacy) {
3195
- const id = z14.object({ id: z14.string() }).parse(row).id;
3256
+ const id = z15.object({ id: z15.string() }).parse(row).id;
3196
3257
  insert.run(
3197
3258
  id,
3198
3259
  JSON.stringify({ id, name: id, path: path7.join(root, id), available: false })
@@ -3402,7 +3463,7 @@ var SqliteSessionStore = class {
3402
3463
  if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
3403
3464
  const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
3404
3465
  if (notification) {
3405
- const count = z14.object({ total: z14.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
3466
+ const count = z15.object({ total: z15.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
3406
3467
  if (count.total >= 1e3)
3407
3468
  this.db.prepare(
3408
3469
  "DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
@@ -3454,7 +3515,7 @@ var SessionStore = class extends Context3.Service()(
3454
3515
  for (const suffix of ["-wal", "-shm"])
3455
3516
  if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
3456
3517
  try {
3457
- const schema = z14.object({ user_version: z14.number() }).parse(db.prepare("PRAGMA user_version").get());
3518
+ const schema = z15.object({ user_version: z15.number() }).parse(db.prepare("PRAGMA user_version").get());
3458
3519
  if (schema.user_version > 1)
3459
3520
  throw new Error("This database requires a newer Agentrific server.");
3460
3521
  db.exec(`
@@ -3583,9 +3644,13 @@ var releaseLock = await acquireServerLock(dataRoot);
3583
3644
  var effects = ManagedRuntime.make(SessionStore.layer(path9.join(dataRoot, "sessions.sqlite")));
3584
3645
  var store = await effects.runPromise(SessionStore);
3585
3646
  var security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
3586
- var push = new ServerPush(store, security.serverId, dataRoot);
3647
+ var testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
3648
+ if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
3649
+ throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
3650
+ var push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
3587
3651
  var allowedOrigins = [
3588
3652
  .../* @__PURE__ */ new Set([
3653
+ "agentrific-desktop://app",
3589
3654
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
3590
3655
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
3591
3656
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -3840,7 +3905,7 @@ app.get("/api/projects/:id", async (request, reply) => {
3840
3905
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
3841
3906
  });
3842
3907
  app.post("/api/projects/:id/reconnect", async (request, reply) => {
3843
- const parsed = z15.object({ path: z15.string().trim().min(1).max(4096) }).safeParse(request.body);
3908
+ const parsed = z16.object({ path: z16.string().trim().min(1).max(4096) }).safeParse(request.body);
3844
3909
  if (!parsed.success)
3845
3910
  return reply.code(400).send({ error: "Enter the project's new folder path." });
3846
3911
  const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
@@ -3972,9 +4037,9 @@ app.post(
3972
4037
  }
3973
4038
  })
3974
4039
  );
3975
- var HistoryQuery = z15.object({
3976
- after: z15.coerce.number().int().nonnegative().default(0),
3977
- limit: z15.coerce.number().int().min(1).max(1e3).default(500)
4040
+ var HistoryQuery = z16.object({
4041
+ after: z16.coerce.number().int().nonnegative().default(0),
4042
+ limit: z16.coerce.number().int().min(1).max(1e3).default(500)
3978
4043
  });
3979
4044
  app.get("/api/sessions/:id/events", async (request, reply) => {
3980
4045
  const query = HistoryQuery.safeParse(request.query);
@@ -4116,7 +4181,7 @@ app.post("/api/sessions", async (request, reply) => {
4116
4181
  if (!result.session) return reply.code(result.status).send({ error: result.error });
4117
4182
  return reply.code(201).send(result.session);
4118
4183
  });
4119
- var StartTaskBody = z15.object({ prompt: z15.string().trim().max(2e4).optional() }).default({});
4184
+ var StartTaskBody = z16.object({ prompt: z16.string().trim().max(2e4).optional() }).default({});
4120
4185
  function startTask(id, instructionsOverride, automated = false) {
4121
4186
  return withTaskLock(
4122
4187
  "agent-dispatch",
@@ -4194,7 +4259,7 @@ app.patch("/api/automation", async (request, reply) => {
4194
4259
  return automation.configure(parsed.data);
4195
4260
  });
4196
4261
  app.patch("/api/automation/queue", async (request, reply) => {
4197
- const parsed = z15.object({ taskId: z15.string().min(1), beforeId: z15.string().min(1).nullable() }).safeParse(request.body);
4262
+ const parsed = z16.object({ taskId: z16.string().min(1), beforeId: z16.string().min(1).nullable() }).safeParse(request.body);
4198
4263
  if (!parsed.success)
4199
4264
  return reply.code(400).send({ error: "Choose a task and a queue position." });
4200
4265
  try {