@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/CHANGELOG.md +8 -0
- package/README.md +2 -0
- package/cli.js +260 -196
- package/cli.js.map +4 -4
- package/index.js +314 -249
- package/index.js.map +4 -4
- package/package.json +1 -1
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
|
|
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(`${
|
|
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({
|
|
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
|
|
909
|
-
import { z as
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
|
1055
|
-
import { z as
|
|
1056
|
-
var MaintenanceSchema =
|
|
1057
|
-
active:
|
|
1058
|
-
drained:
|
|
1059
|
-
blockers:
|
|
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 =
|
|
1062
|
-
serverId:
|
|
1063
|
-
publicUrl:
|
|
1064
|
-
serverVersion:
|
|
1065
|
-
connectedDevices:
|
|
1066
|
-
connectedSockets:
|
|
1067
|
-
memory:
|
|
1068
|
-
activeRuns:
|
|
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 =
|
|
1072
|
-
|
|
1073
|
-
pairingId:
|
|
1074
|
-
pin:
|
|
1075
|
-
expiresAt:
|
|
1076
|
-
serverId:
|
|
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
|
-
|
|
967
|
+
z4.object({ revoked: z4.boolean() }),
|
|
1081
968
|
MaintenanceSchema
|
|
1082
969
|
]);
|
|
1083
|
-
var ControlRequest =
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
command:
|
|
1090
|
-
action:
|
|
1091
|
-
timeoutMs:
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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.
|
|
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
|
|
1377
|
+
import { z as z7 } from "zod";
|
|
1317
1378
|
import { Effect as Effect5 } from "effect";
|
|
1318
|
-
var RegistryRelease =
|
|
1319
|
-
version:
|
|
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
|
|
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
|
|
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
|
|
1959
|
-
var DeliveryJobSchema =
|
|
1960
|
-
kind:
|
|
1961
|
-
sessionId:
|
|
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:
|
|
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:
|
|
1974
|
-
commitSha:
|
|
1975
|
-
title:
|
|
1976
|
-
body:
|
|
1977
|
-
logs:
|
|
1978
|
-
failed:
|
|
1979
|
-
result:
|
|
1980
|
-
url:
|
|
1981
|
-
message:
|
|
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 =
|
|
1984
|
-
kind:
|
|
1985
|
-
sessionId:
|
|
1986
|
-
projectPath:
|
|
2044
|
+
var PreparationJobSchema = z8.object({
|
|
2045
|
+
kind: z8.literal("preparation"),
|
|
2046
|
+
sessionId: z8.string(),
|
|
2047
|
+
projectPath: z8.string(),
|
|
1987
2048
|
input: CreateSessionSchema,
|
|
1988
|
-
stage:
|
|
1989
|
-
logs:
|
|
2049
|
+
stage: z8.enum(["creating", "installing", "ready", "failed"]),
|
|
2050
|
+
logs: z8.string().default("")
|
|
1990
2051
|
});
|
|
1991
|
-
var WorkflowJobSchema =
|
|
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 =
|
|
2288
|
-
|
|
2289
|
-
url:
|
|
2290
|
-
state:
|
|
2291
|
-
headRefOid:
|
|
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
|
|
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 =
|
|
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
|
|
2536
|
-
var DraftSchema =
|
|
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
|
|
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 =
|
|
2727
|
-
content:
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
3032
|
-
var PushBindingSchema =
|
|
3033
|
-
bindingId:
|
|
3034
|
-
installationId:
|
|
3035
|
-
deviceId:
|
|
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 =
|
|
3038
|
-
bindings:
|
|
3039
|
-
error:
|
|
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:
|
|
3043
|
-
createdAt:
|
|
3044
|
-
attempts:
|
|
3045
|
-
nextAt:
|
|
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
|
|
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 =
|
|
3056
|
-
var EventRow =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
3976
|
-
after:
|
|
3977
|
-
limit:
|
|
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 =
|
|
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 =
|
|
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 {
|