@agentrific/server 0.1.5 → 0.2.0
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 +14 -0
- package/README.md +2 -0
- package/cli.js +439 -261
- package/cli.js.map +4 -4
- package/index.js +435 -264
- package/index.js.map +4 -4
- package/package.json +2 -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)) {
|
|
@@ -475,11 +492,18 @@ var CreateSessionSchema = z2.object({
|
|
|
475
492
|
var SendMessageSchema = z2.object({
|
|
476
493
|
message: z2.string().trim().max(2e4),
|
|
477
494
|
attachmentIds: AttachmentIdsSchema.optional(),
|
|
478
|
-
clientMessageId: z2.string().min(1).max(120).optional()
|
|
495
|
+
clientMessageId: z2.string().min(1).max(120).optional(),
|
|
496
|
+
mode: z2.enum(["followUp", "steer"]).optional()
|
|
479
497
|
}).refine(
|
|
480
498
|
(input) => input.message.length > 0 || (input.attachmentIds?.length ?? 0) > 0,
|
|
481
499
|
"Write a message or attach a file."
|
|
482
500
|
);
|
|
501
|
+
var FileChangeSchema = z2.object({
|
|
502
|
+
path: z2.string(),
|
|
503
|
+
patch: z2.string().optional(),
|
|
504
|
+
content: z2.string().optional(),
|
|
505
|
+
omitted: z2.boolean().optional()
|
|
506
|
+
});
|
|
483
507
|
var SessionEventSchema = z2.discriminatedUnion("type", [
|
|
484
508
|
z2.object({ type: z2.literal("session.updated"), session: SessionSchema }),
|
|
485
509
|
z2.object({
|
|
@@ -514,7 +538,8 @@ var SessionEventSchema = z2.discriminatedUnion("type", [
|
|
|
514
538
|
sessionId: z2.string(),
|
|
515
539
|
toolCallId: z2.string().optional(),
|
|
516
540
|
tool: z2.string(),
|
|
517
|
-
input: z2.string()
|
|
541
|
+
input: z2.string(),
|
|
542
|
+
filePath: z2.string().optional()
|
|
518
543
|
}),
|
|
519
544
|
z2.object({
|
|
520
545
|
type: z2.literal("tool.completed"),
|
|
@@ -522,6 +547,7 @@ var SessionEventSchema = z2.discriminatedUnion("type", [
|
|
|
522
547
|
toolCallId: z2.string().optional(),
|
|
523
548
|
tool: z2.string(),
|
|
524
549
|
output: z2.string(),
|
|
550
|
+
fileChange: FileChangeSchema.optional(),
|
|
525
551
|
isError: z2.boolean().default(false)
|
|
526
552
|
}),
|
|
527
553
|
z2.object({
|
|
@@ -613,6 +639,11 @@ var PairingPayloadSchema = z2.object({
|
|
|
613
639
|
serverId: z2.string().uuid(),
|
|
614
640
|
pairingId: z2.string().regex(/^[a-f0-9]{64}$/)
|
|
615
641
|
});
|
|
642
|
+
var PairingInvitationSchema = z2.object({
|
|
643
|
+
payload: PairingPayloadSchema,
|
|
644
|
+
pin: z2.string().regex(/^\d{6}$/),
|
|
645
|
+
expiresAt: z2.number()
|
|
646
|
+
});
|
|
616
647
|
var PairingRequestSchema = z2.object({
|
|
617
648
|
pairingId: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
618
649
|
pin: z2.string().regex(/^\d{6}$/),
|
|
@@ -905,190 +936,55 @@ function attachEventServer(app2, security2, origins, snapshot) {
|
|
|
905
936
|
|
|
906
937
|
// src/security.ts
|
|
907
938
|
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
|
-
}
|
|
939
|
+
import { Effect as Effect4 } from "effect";
|
|
940
|
+
import { z as z6 } from "zod";
|
|
941
|
+
|
|
942
|
+
// src/pairing.ts
|
|
943
|
+
import { z as z5 } from "zod";
|
|
944
|
+
import qrcode from "qrcode-terminal";
|
|
1049
945
|
|
|
1050
946
|
// src/control.ts
|
|
1051
947
|
import { createServer, createConnection } from "node:net";
|
|
1052
948
|
import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
|
|
1053
949
|
import path3 from "node:path";
|
|
1054
|
-
import { Effect as
|
|
1055
|
-
import { z as
|
|
1056
|
-
var MaintenanceSchema =
|
|
1057
|
-
active:
|
|
1058
|
-
drained:
|
|
1059
|
-
blockers:
|
|
950
|
+
import { Effect as Effect3 } from "effect";
|
|
951
|
+
import { z as z4 } from "zod";
|
|
952
|
+
var MaintenanceSchema = z4.object({
|
|
953
|
+
active: z4.boolean(),
|
|
954
|
+
drained: z4.boolean(),
|
|
955
|
+
blockers: z4.array(z4.string())
|
|
1060
956
|
});
|
|
1061
|
-
var ControlStatusSchema =
|
|
1062
|
-
serverId:
|
|
1063
|
-
publicUrl:
|
|
1064
|
-
serverVersion:
|
|
1065
|
-
connectedDevices:
|
|
1066
|
-
connectedSockets:
|
|
1067
|
-
memory:
|
|
1068
|
-
activeRuns:
|
|
957
|
+
var ControlStatusSchema = z4.object({
|
|
958
|
+
serverId: z4.string().uuid(),
|
|
959
|
+
publicUrl: z4.string().optional(),
|
|
960
|
+
serverVersion: z4.string().optional(),
|
|
961
|
+
connectedDevices: z4.number().optional(),
|
|
962
|
+
connectedSockets: z4.number().optional(),
|
|
963
|
+
memory: z4.number().optional(),
|
|
964
|
+
activeRuns: z4.number().optional(),
|
|
1069
965
|
maintenance: MaintenanceSchema.optional()
|
|
1070
966
|
});
|
|
1071
|
-
var ControlResultSchema =
|
|
1072
|
-
|
|
1073
|
-
pairingId:
|
|
1074
|
-
pin:
|
|
1075
|
-
expiresAt:
|
|
1076
|
-
serverId:
|
|
967
|
+
var ControlResultSchema = z4.union([
|
|
968
|
+
z4.object({
|
|
969
|
+
pairingId: z4.string(),
|
|
970
|
+
pin: z4.string(),
|
|
971
|
+
expiresAt: z4.number(),
|
|
972
|
+
serverId: z4.string().uuid()
|
|
1077
973
|
}),
|
|
1078
974
|
DeviceSchema.array(),
|
|
1079
975
|
ControlStatusSchema,
|
|
1080
|
-
|
|
976
|
+
z4.object({ revoked: z4.boolean() }),
|
|
1081
977
|
MaintenanceSchema
|
|
1082
978
|
]);
|
|
1083
|
-
var ControlRequest =
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
command:
|
|
1090
|
-
action:
|
|
1091
|
-
timeoutMs:
|
|
979
|
+
var ControlRequest = z4.discriminatedUnion("command", [
|
|
980
|
+
z4.object({ command: z4.literal("pair") }),
|
|
981
|
+
z4.object({ command: z4.literal("devices") }),
|
|
982
|
+
z4.object({ command: z4.literal("status") }),
|
|
983
|
+
z4.object({ command: z4.literal("revoke"), id: z4.string().uuid() }),
|
|
984
|
+
z4.object({
|
|
985
|
+
command: z4.literal("maintenance"),
|
|
986
|
+
action: z4.enum(["enter", "resume", "drain"]),
|
|
987
|
+
timeoutMs: z4.number().int().min(0).max(6e4).default(3e4)
|
|
1092
988
|
})
|
|
1093
989
|
]);
|
|
1094
990
|
async function acquireServerLock(directory) {
|
|
@@ -1098,14 +994,14 @@ async function acquireServerLock(directory) {
|
|
|
1098
994
|
try {
|
|
1099
995
|
await writeFile(filename, String(process.pid), { flag: "wx", mode: 384 });
|
|
1100
996
|
} catch (cause) {
|
|
1101
|
-
if (!
|
|
997
|
+
if (!z4.object({ code: z4.literal("EEXIST") }).safeParse(cause).success) throw cause;
|
|
1102
998
|
const owner = Number(await readFile2(filename, "utf8"));
|
|
1103
999
|
if (!Number.isInteger(owner) || owner <= 0)
|
|
1104
1000
|
throw new Error("Invalid server lock. Check the owner before removing it.");
|
|
1105
1001
|
try {
|
|
1106
1002
|
process.kill(owner, 0);
|
|
1107
1003
|
} catch (error) {
|
|
1108
|
-
if (!
|
|
1004
|
+
if (!z4.object({ code: z4.literal("ESRCH") }).safeParse(error).success) throw error;
|
|
1109
1005
|
const recovery = `${filename}.recovery`;
|
|
1110
1006
|
await writeFile(recovery, String(process.pid), { flag: "wx", mode: 384 }).catch(() => {
|
|
1111
1007
|
throw new Error(
|
|
@@ -1135,7 +1031,7 @@ async function acquireServerLock(directory) {
|
|
|
1135
1031
|
async function startControl(directory, security2, status, maintenance2) {
|
|
1136
1032
|
const filename = path3.join(directory, "control.sock");
|
|
1137
1033
|
await unlink(filename).catch((error) => {
|
|
1138
|
-
if (!
|
|
1034
|
+
if (!z4.object({ code: z4.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
1139
1035
|
});
|
|
1140
1036
|
const server = createServer((socket) => {
|
|
1141
1037
|
socket.setTimeout(5e3, () => socket.destroy());
|
|
@@ -1157,7 +1053,7 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
1157
1053
|
result = security2.createPairing();
|
|
1158
1054
|
break;
|
|
1159
1055
|
case "devices":
|
|
1160
|
-
result = await
|
|
1056
|
+
result = await Effect3.runPromise(security2.store.devices());
|
|
1161
1057
|
break;
|
|
1162
1058
|
case "status":
|
|
1163
1059
|
result = status();
|
|
@@ -1194,7 +1090,7 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
1194
1090
|
// package.json
|
|
1195
1091
|
var package_default = {
|
|
1196
1092
|
name: "@agentrific/server",
|
|
1197
|
-
version: "0.
|
|
1093
|
+
version: "0.2.0",
|
|
1198
1094
|
description: "Self-hosted control plane for long-running coding agents",
|
|
1199
1095
|
keywords: [
|
|
1200
1096
|
"agentrific",
|
|
@@ -1231,6 +1127,7 @@ var package_default = {
|
|
|
1231
1127
|
"@fastify/cors": "^11.3.0",
|
|
1232
1128
|
"@fastify/multipart": "^10.1.2",
|
|
1233
1129
|
"@inquirer/select": "^5.2.5",
|
|
1130
|
+
diff: "^9.0.0",
|
|
1234
1131
|
effect: "4.0.0-rc.117",
|
|
1235
1132
|
fastify: "^5.6.2",
|
|
1236
1133
|
"pino-pretty": "^13.1.3",
|
|
@@ -1257,6 +1154,180 @@ var package_default = {
|
|
|
1257
1154
|
// src/version.ts
|
|
1258
1155
|
var serverVersion = package_default.version;
|
|
1259
1156
|
|
|
1157
|
+
// src/pairing.ts
|
|
1158
|
+
function pairingOrigin(url) {
|
|
1159
|
+
const endpoint = new URL(url);
|
|
1160
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
1161
|
+
throw new Error("Use the HTTPS server origin without a path or credentials.");
|
|
1162
|
+
if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
|
|
1163
|
+
throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
|
|
1164
|
+
return endpoint.origin;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/security.ts
|
|
1168
|
+
var secret = () => randomBytes(32).toString("hex");
|
|
1169
|
+
var digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
1170
|
+
var ServerSecurity = class {
|
|
1171
|
+
constructor(store2, serverId) {
|
|
1172
|
+
this.store = store2;
|
|
1173
|
+
this.serverId = serverId;
|
|
1174
|
+
}
|
|
1175
|
+
store;
|
|
1176
|
+
serverId;
|
|
1177
|
+
grants = /* @__PURE__ */ new Map();
|
|
1178
|
+
tickets = /* @__PURE__ */ new Map();
|
|
1179
|
+
attempts = /* @__PURE__ */ new Map();
|
|
1180
|
+
sockets = /* @__PURE__ */ new Map();
|
|
1181
|
+
prune() {
|
|
1182
|
+
const now = Date.now();
|
|
1183
|
+
for (const [key, grant] of this.grants) if (grant.expiresAt <= now) this.grants.delete(key);
|
|
1184
|
+
for (const [key, ticket] of this.tickets) if (ticket.expiresAt <= now) this.tickets.delete(key);
|
|
1185
|
+
for (const [key, attempts] of this.attempts)
|
|
1186
|
+
if (attempts.until <= now) this.attempts.delete(key);
|
|
1187
|
+
}
|
|
1188
|
+
createPairing() {
|
|
1189
|
+
this.prune();
|
|
1190
|
+
if (this.grants.size >= 10)
|
|
1191
|
+
throw new Error("Too many pending enrollments. Wait for them to expire.");
|
|
1192
|
+
const pairingId = secret();
|
|
1193
|
+
const grant = {
|
|
1194
|
+
pin: String(randomInt(1e6)).padStart(6, "0"),
|
|
1195
|
+
expiresAt: Date.now() + 3e5,
|
|
1196
|
+
attempts: 0
|
|
1197
|
+
};
|
|
1198
|
+
this.grants.set(pairingId, grant);
|
|
1199
|
+
return { pairingId, pin: grant.pin, expiresAt: grant.expiresAt, serverId: this.serverId };
|
|
1200
|
+
}
|
|
1201
|
+
async pair(input, address) {
|
|
1202
|
+
this.prune();
|
|
1203
|
+
const attempts = this.attempts.get(address) ?? { count: 0, until: Date.now() + 6e4 };
|
|
1204
|
+
if (attempts.count >= 20 || !this.attempts.has(address) && this.attempts.size >= 5e3)
|
|
1205
|
+
return void 0;
|
|
1206
|
+
attempts.count++;
|
|
1207
|
+
this.attempts.set(address, attempts);
|
|
1208
|
+
const grant = this.grants.get(input.pairingId);
|
|
1209
|
+
if (!grant) return void 0;
|
|
1210
|
+
grant.attempts++;
|
|
1211
|
+
const match = timingSafeEqual(Buffer.from(grant.pin), Buffer.from(input.pin));
|
|
1212
|
+
if (grant.attempts >= 3 || match) this.grants.delete(input.pairingId);
|
|
1213
|
+
if (!match) return void 0;
|
|
1214
|
+
const token = secret();
|
|
1215
|
+
const now = Date.now();
|
|
1216
|
+
const device = {
|
|
1217
|
+
id: randomUUID3(),
|
|
1218
|
+
name: input.deviceName,
|
|
1219
|
+
platform: input.platform,
|
|
1220
|
+
createdAt: now,
|
|
1221
|
+
lastSeenAt: now,
|
|
1222
|
+
revokedAt: null
|
|
1223
|
+
};
|
|
1224
|
+
await Effect4.runPromise(this.store.saveDevice(device, digest(token)));
|
|
1225
|
+
return { deviceId: device.id, deviceToken: token, serverId: this.serverId };
|
|
1226
|
+
}
|
|
1227
|
+
authenticate(token) {
|
|
1228
|
+
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1229
|
+
return Effect4.runSync(this.store.deviceForToken(digest(token)));
|
|
1230
|
+
}
|
|
1231
|
+
ticket(token, path10) {
|
|
1232
|
+
this.prune();
|
|
1233
|
+
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1234
|
+
const value = secret();
|
|
1235
|
+
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path10 });
|
|
1236
|
+
return value;
|
|
1237
|
+
}
|
|
1238
|
+
redeemTicket(value, path10, consume = true) {
|
|
1239
|
+
this.prune();
|
|
1240
|
+
const ticket = this.tickets.get(value);
|
|
1241
|
+
if (!ticket || ticket.path !== path10) return void 0;
|
|
1242
|
+
if (consume) this.tickets.delete(value);
|
|
1243
|
+
return Effect4.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1244
|
+
}
|
|
1245
|
+
async revoke(id) {
|
|
1246
|
+
const revoked = await Effect4.runPromise(this.store.revokeDevice(id));
|
|
1247
|
+
for (const [socket, owner] of this.sockets)
|
|
1248
|
+
if (owner === id) socket.close(4401, "Device revoked");
|
|
1249
|
+
return revoked;
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1253
|
+
app2.addHook("onRequest", async (request, reply) => {
|
|
1254
|
+
const path10 = new URL(request.url, "http://localhost");
|
|
1255
|
+
reply.header("Cache-Control", "no-store");
|
|
1256
|
+
if (request.method === "OPTIONS") return;
|
|
1257
|
+
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1258
|
+
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1259
|
+
if (path10.pathname === "/health" || path10.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1260
|
+
return;
|
|
1261
|
+
const authorization = request.headers.authorization ?? "";
|
|
1262
|
+
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
1263
|
+
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path10.pathname);
|
|
1264
|
+
const ticket = path10.searchParams.get("ticket");
|
|
1265
|
+
path10.searchParams.delete("ticket");
|
|
1266
|
+
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path10.pathname + path10.search, false)))
|
|
1267
|
+
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1268
|
+
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1269
|
+
return reply.code(409).send({
|
|
1270
|
+
error: "Update your Agentrific client or server.",
|
|
1271
|
+
code: "VERSION_MISMATCH",
|
|
1272
|
+
protocolVersion: PROTOCOL_VERSION
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
app2.get("/health", () => ({
|
|
1276
|
+
status: "ok",
|
|
1277
|
+
service: "agentrific-server",
|
|
1278
|
+
serverId: security2.serverId,
|
|
1279
|
+
serverVersion: version,
|
|
1280
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1281
|
+
authentication: "pairing"
|
|
1282
|
+
}));
|
|
1283
|
+
app2.post("/api/auth/pair", async (request, reply) => {
|
|
1284
|
+
const input = PairingRequestSchema.safeParse(request.body);
|
|
1285
|
+
if (!input.success)
|
|
1286
|
+
return reply.code(400).send({ error: "Enter a valid pairing payload and six-digit PIN." });
|
|
1287
|
+
const result = await security2.pair(input.data, request.ip);
|
|
1288
|
+
return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
|
|
1289
|
+
});
|
|
1290
|
+
app2.post("/api/auth/pairing-invitation", (_request, reply) => {
|
|
1291
|
+
const publicUrl = process.env.AGENTRIFIC_PUBLIC_URL;
|
|
1292
|
+
if (!publicUrl)
|
|
1293
|
+
return reply.code(503).send({
|
|
1294
|
+
error: "Configure the server\u2019s public HTTPS URL and restart the server before pairing a device."
|
|
1295
|
+
});
|
|
1296
|
+
let url;
|
|
1297
|
+
try {
|
|
1298
|
+
url = pairingOrigin(publicUrl);
|
|
1299
|
+
} catch {
|
|
1300
|
+
return reply.code(503).send({ error: "The server\u2019s public URL is invalid. Configure a public HTTPS origin." });
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
const grant = security2.createPairing();
|
|
1304
|
+
return {
|
|
1305
|
+
payload: { v: 1, url, serverId: grant.serverId, pairingId: grant.pairingId },
|
|
1306
|
+
pin: grant.pin,
|
|
1307
|
+
expiresAt: grant.expiresAt
|
|
1308
|
+
};
|
|
1309
|
+
} catch {
|
|
1310
|
+
return reply.code(429).send({ error: "Too many pending pairing codes. Wait five minutes and try again." });
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
app2.get("/api/auth/session", (request) => ({
|
|
1314
|
+
serverId: security2.serverId,
|
|
1315
|
+
device: security2.authenticate((request.headers.authorization ?? "").slice(7))
|
|
1316
|
+
}));
|
|
1317
|
+
app2.post("/api/auth/event-ticket", (request) => ({
|
|
1318
|
+
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
|
|
1319
|
+
}));
|
|
1320
|
+
app2.post("/api/auth/asset-ticket", (request, reply) => {
|
|
1321
|
+
const input = z6.object({
|
|
1322
|
+
path: z6.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
|
|
1323
|
+
}).safeParse(request.body);
|
|
1324
|
+
if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
|
|
1325
|
+
return {
|
|
1326
|
+
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), input.data.path)
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1260
1331
|
// src/maintenance.ts
|
|
1261
1332
|
var MaintenanceError = class extends Error {
|
|
1262
1333
|
statusCode = 503;
|
|
@@ -1313,10 +1384,10 @@ var Maintenance = class {
|
|
|
1313
1384
|
|
|
1314
1385
|
// src/updates.ts
|
|
1315
1386
|
import semver from "semver";
|
|
1316
|
-
import { z as
|
|
1387
|
+
import { z as z7 } from "zod";
|
|
1317
1388
|
import { Effect as Effect5 } from "effect";
|
|
1318
|
-
var RegistryRelease =
|
|
1319
|
-
version:
|
|
1389
|
+
var RegistryRelease = z7.object({
|
|
1390
|
+
version: z7.string().refine((value) => semver.valid(value) !== null)
|
|
1320
1391
|
});
|
|
1321
1392
|
async function checkForUpdates() {
|
|
1322
1393
|
const response = await fetch("https://registry.npmjs.org/@agentrific%2Fserver/latest", {
|
|
@@ -1406,7 +1477,7 @@ var UpdateDiscovery = class {
|
|
|
1406
1477
|
|
|
1407
1478
|
// src/index.ts
|
|
1408
1479
|
import { Effect as Effect16, ManagedRuntime } from "effect";
|
|
1409
|
-
import { z as
|
|
1480
|
+
import { z as z17 } from "zod";
|
|
1410
1481
|
|
|
1411
1482
|
// src/automation.ts
|
|
1412
1483
|
import { Effect as Effect7 } from "effect";
|
|
@@ -1768,7 +1839,7 @@ var AgentController = class {
|
|
|
1768
1839
|
Effect8.ensuring(Effect8.sync(() => this.working.delete(entry.session.id)))
|
|
1769
1840
|
);
|
|
1770
1841
|
}
|
|
1771
|
-
send(session, message, attachments2 = [], messageId = randomUUID4()) {
|
|
1842
|
+
send(session, message, attachments2 = [], messageId = randomUUID4(), mode = "followUp") {
|
|
1772
1843
|
return this.lock(session.id).withPermit(
|
|
1773
1844
|
Effect8.gen({ self: this }, function* () {
|
|
1774
1845
|
if (this.closed)
|
|
@@ -1778,6 +1849,16 @@ var AgentController = class {
|
|
|
1778
1849
|
if (this.hasMessage(session.id, messageId)) return;
|
|
1779
1850
|
const entry = yield* this.open(session);
|
|
1780
1851
|
yield* this.operation(async () => entry.agent.validateAttachments?.(attachments2));
|
|
1852
|
+
const steering = entry.running && mode === "steer";
|
|
1853
|
+
if (steering) {
|
|
1854
|
+
if (!entry.agent.steer)
|
|
1855
|
+
return yield* Effect8.fail(
|
|
1856
|
+
new AgentError({ message: "This agent does not support steering." })
|
|
1857
|
+
);
|
|
1858
|
+
yield* this.operation(
|
|
1859
|
+
() => entry.agent.steer(gitFlowPrompt(entry.session, message), attachments2)
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1781
1862
|
this.emit({
|
|
1782
1863
|
type: "message.user",
|
|
1783
1864
|
sessionId: session.id,
|
|
@@ -1785,6 +1866,7 @@ var AgentController = class {
|
|
|
1785
1866
|
text: message,
|
|
1786
1867
|
attachments: attachments2
|
|
1787
1868
|
});
|
|
1869
|
+
if (steering) return;
|
|
1788
1870
|
if (entry.running) {
|
|
1789
1871
|
entry.pending.push({ text: message, attachments: attachments2 });
|
|
1790
1872
|
return;
|
|
@@ -1911,7 +1993,7 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
1911
1993
|
|
|
1912
1994
|
// src/delivery.ts
|
|
1913
1995
|
import { Effect as Effect9 } from "effect";
|
|
1914
|
-
import { z as
|
|
1996
|
+
import { z as z9 } from "zod";
|
|
1915
1997
|
|
|
1916
1998
|
// src/workflow-command.ts
|
|
1917
1999
|
import { execFile } from "node:child_process";
|
|
@@ -1955,12 +2037,12 @@ function workflowError(error) {
|
|
|
1955
2037
|
}
|
|
1956
2038
|
|
|
1957
2039
|
// src/workflow-schema.ts
|
|
1958
|
-
import { z as
|
|
1959
|
-
var DeliveryJobSchema =
|
|
1960
|
-
kind:
|
|
1961
|
-
sessionId:
|
|
2040
|
+
import { z as z8 } from "zod";
|
|
2041
|
+
var DeliveryJobSchema = z8.object({
|
|
2042
|
+
kind: z8.literal("delivery"),
|
|
2043
|
+
sessionId: z8.string(),
|
|
1962
2044
|
request: DeliveryRequestSchema,
|
|
1963
|
-
phase:
|
|
2045
|
+
phase: z8.enum([
|
|
1964
2046
|
"validating",
|
|
1965
2047
|
"drafting",
|
|
1966
2048
|
"committing",
|
|
@@ -1970,25 +2052,25 @@ var DeliveryJobSchema = z7.object({
|
|
|
1970
2052
|
"opening_pr",
|
|
1971
2053
|
"delivered"
|
|
1972
2054
|
]),
|
|
1973
|
-
attempt:
|
|
1974
|
-
commitSha:
|
|
1975
|
-
title:
|
|
1976
|
-
body:
|
|
1977
|
-
logs:
|
|
1978
|
-
failed:
|
|
1979
|
-
result:
|
|
1980
|
-
url:
|
|
1981
|
-
message:
|
|
2055
|
+
attempt: z8.number().int().positive(),
|
|
2056
|
+
commitSha: z8.string().optional(),
|
|
2057
|
+
title: z8.string(),
|
|
2058
|
+
body: z8.string(),
|
|
2059
|
+
logs: z8.string().default(""),
|
|
2060
|
+
failed: z8.boolean().default(false),
|
|
2061
|
+
result: z8.enum(["merged", "pr_opened", "no_changes"]).optional(),
|
|
2062
|
+
url: z8.string().optional(),
|
|
2063
|
+
message: z8.string().optional()
|
|
1982
2064
|
});
|
|
1983
|
-
var PreparationJobSchema =
|
|
1984
|
-
kind:
|
|
1985
|
-
sessionId:
|
|
1986
|
-
projectPath:
|
|
2065
|
+
var PreparationJobSchema = z8.object({
|
|
2066
|
+
kind: z8.literal("preparation"),
|
|
2067
|
+
sessionId: z8.string(),
|
|
2068
|
+
projectPath: z8.string(),
|
|
1987
2069
|
input: CreateSessionSchema,
|
|
1988
|
-
stage:
|
|
1989
|
-
logs:
|
|
2070
|
+
stage: z8.enum(["creating", "installing", "ready", "failed"]),
|
|
2071
|
+
logs: z8.string().default("")
|
|
1990
2072
|
});
|
|
1991
|
-
var WorkflowJobSchema =
|
|
2073
|
+
var WorkflowJobSchema = z8.discriminatedUnion("kind", [
|
|
1992
2074
|
DeliveryJobSchema,
|
|
1993
2075
|
PreparationJobSchema
|
|
1994
2076
|
]);
|
|
@@ -2284,11 +2366,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2284
2366
|
"--json",
|
|
2285
2367
|
"url,state,headRefOid"
|
|
2286
2368
|
]);
|
|
2287
|
-
const prs =
|
|
2288
|
-
|
|
2289
|
-
url:
|
|
2290
|
-
state:
|
|
2291
|
-
headRefOid:
|
|
2369
|
+
const prs = z9.array(
|
|
2370
|
+
z9.object({
|
|
2371
|
+
url: z9.string().url(),
|
|
2372
|
+
state: z9.string(),
|
|
2373
|
+
headRefOid: z9.string().optional()
|
|
2292
2374
|
})
|
|
2293
2375
|
).parse(JSON.parse(existing));
|
|
2294
2376
|
const open = prs.find(
|
|
@@ -2332,7 +2414,7 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2332
2414
|
import { mkdir as mkdir3, readFile as readFile3 } from "node:fs/promises";
|
|
2333
2415
|
import path5 from "node:path";
|
|
2334
2416
|
import { Effect as Effect10 } from "effect";
|
|
2335
|
-
import { z as
|
|
2417
|
+
import { z as z10 } from "zod";
|
|
2336
2418
|
var PreparationCoordinator = class {
|
|
2337
2419
|
constructor(store2, options) {
|
|
2338
2420
|
this.store = store2;
|
|
@@ -2450,7 +2532,7 @@ ${logs}` : ""}`);
|
|
|
2450
2532
|
() => void 0
|
|
2451
2533
|
);
|
|
2452
2534
|
if (!packageFile) return;
|
|
2453
|
-
const metadata =
|
|
2535
|
+
const metadata = z10.object({ packageManager: z10.string().optional() }).parse(JSON.parse(packageFile));
|
|
2454
2536
|
const has = async (name) => readFile3(path5.join(workspace, name)).then(
|
|
2455
2537
|
() => true,
|
|
2456
2538
|
() => false
|
|
@@ -2532,8 +2614,8 @@ ${logs}` : ""}`);
|
|
|
2532
2614
|
|
|
2533
2615
|
// src/delivery-writer.ts
|
|
2534
2616
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2535
|
-
import { z as
|
|
2536
|
-
var DraftSchema =
|
|
2617
|
+
import { z as z11 } from "zod";
|
|
2618
|
+
var DraftSchema = z11.object({ title: z11.string().trim().min(1).max(120), body: z11.string().trim().min(1).max(1e4) }).strict();
|
|
2537
2619
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2538
2620
|
return async (run, input, diff, signal) => {
|
|
2539
2621
|
signal?.throwIfAborted();
|
|
@@ -2688,7 +2770,7 @@ import {
|
|
|
2688
2770
|
createAgentSession as createAgentSession2,
|
|
2689
2771
|
SessionManager as SessionManager2
|
|
2690
2772
|
} from "@earendil-works/pi-coding-agent";
|
|
2691
|
-
import { z as
|
|
2773
|
+
import { z as z13 } from "zod";
|
|
2692
2774
|
|
|
2693
2775
|
// src/delivery-tool.ts
|
|
2694
2776
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -2722,9 +2804,70 @@ function deliveryTool(sessionId, delivery2) {
|
|
|
2722
2804
|
});
|
|
2723
2805
|
}
|
|
2724
2806
|
|
|
2807
|
+
// src/file-change.ts
|
|
2808
|
+
import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
2809
|
+
import { createTwoFilesPatch } from "diff";
|
|
2810
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
2811
|
+
import { z as z12 } from "zod";
|
|
2812
|
+
var previewLimit = 256e3;
|
|
2813
|
+
function boundFileChange(change) {
|
|
2814
|
+
if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
|
|
2815
|
+
return { path: change.path, omitted: true };
|
|
2816
|
+
return change;
|
|
2817
|
+
}
|
|
2818
|
+
var EditResultSchema = z12.object({
|
|
2819
|
+
details: z12.object({ patch: z12.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
|
|
2820
|
+
});
|
|
2821
|
+
function completedFileChange(result, path10) {
|
|
2822
|
+
const parsed = EditResultSchema.safeParse(result);
|
|
2823
|
+
if (!parsed.success) return;
|
|
2824
|
+
const details = parsed.data.details;
|
|
2825
|
+
if (details?.fileChange) return boundFileChange(details.fileChange);
|
|
2826
|
+
if (path10 && details?.patch) return boundFileChange({ path: path10, patch: details.patch });
|
|
2827
|
+
}
|
|
2828
|
+
function fileWriteTool(cwd) {
|
|
2829
|
+
const base = createWriteToolDefinition(cwd);
|
|
2830
|
+
return defineTool2({
|
|
2831
|
+
name: base.name,
|
|
2832
|
+
label: base.label,
|
|
2833
|
+
description: base.description,
|
|
2834
|
+
promptSnippet: base.promptSnippet,
|
|
2835
|
+
promptGuidelines: base.promptGuidelines,
|
|
2836
|
+
parameters: base.parameters,
|
|
2837
|
+
constrainedSampling: base.constrainedSampling,
|
|
2838
|
+
async execute(toolCallId, args, signal, _onUpdate, ctx) {
|
|
2839
|
+
let fileChange;
|
|
2840
|
+
const tool = createWriteToolDefinition(cwd, {
|
|
2841
|
+
operations: {
|
|
2842
|
+
mkdir: async (dir) => {
|
|
2843
|
+
await mkdir4(dir, { recursive: true });
|
|
2844
|
+
},
|
|
2845
|
+
// Pi invokes this inside its per-file mutation queue, so the snapshot
|
|
2846
|
+
// and write cannot race another built-in edit/write on the same file.
|
|
2847
|
+
writeFile: async (path10, content) => {
|
|
2848
|
+
const previous = await readFile4(path10, "utf8").catch((error) => {
|
|
2849
|
+
if (error.code === "ENOENT") return void 0;
|
|
2850
|
+
throw error;
|
|
2851
|
+
});
|
|
2852
|
+
await writeFile2(path10, content, "utf8");
|
|
2853
|
+
fileChange = boundFileChange(
|
|
2854
|
+
previous === void 0 ? { path: args.path, content } : {
|
|
2855
|
+
path: args.path,
|
|
2856
|
+
patch: createTwoFilesPatch(args.path, args.path, previous, content)
|
|
2857
|
+
}
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
});
|
|
2862
|
+
const result = await tool.execute(toolCallId, args, signal, void 0, ctx);
|
|
2863
|
+
return { ...result, details: { fileChange } };
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2725
2868
|
// src/pi.ts
|
|
2726
|
-
var ToolResultSchema =
|
|
2727
|
-
content:
|
|
2869
|
+
var ToolResultSchema = z13.object({
|
|
2870
|
+
content: z13.array(z13.object({ type: z13.string(), text: z13.string().optional() })).optional()
|
|
2728
2871
|
}).passthrough();
|
|
2729
2872
|
var boundedJson = (value, limit) => {
|
|
2730
2873
|
let text;
|
|
@@ -2749,7 +2892,10 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2749
2892
|
model,
|
|
2750
2893
|
modelRuntime: models2.runtime,
|
|
2751
2894
|
sessionManager: SessionManager2.continueRecent(cwd, sessionDirectory),
|
|
2752
|
-
customTools:
|
|
2895
|
+
customTools: [
|
|
2896
|
+
fileWriteTool(cwd),
|
|
2897
|
+
...run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
|
|
2898
|
+
]
|
|
2753
2899
|
});
|
|
2754
2900
|
return {
|
|
2755
2901
|
get model() {
|
|
@@ -2773,6 +2919,12 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2773
2919
|
throw new Error(last.errorMessage ?? "The model request failed");
|
|
2774
2920
|
}
|
|
2775
2921
|
},
|
|
2922
|
+
async steer(message, attachments2 = []) {
|
|
2923
|
+
const context = await Effect12.runPromise(files.prepare(attachments2));
|
|
2924
|
+
if (!session.isStreaming)
|
|
2925
|
+
throw new Error("The response has finished. Send this message as a follow-up instead.");
|
|
2926
|
+
await session.steer(message + context.context, context.images);
|
|
2927
|
+
},
|
|
2776
2928
|
async abort() {
|
|
2777
2929
|
session.clearQueue();
|
|
2778
2930
|
await session.abort();
|
|
@@ -2837,16 +2989,23 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2837
2989
|
}
|
|
2838
2990
|
return false;
|
|
2839
2991
|
}
|
|
2992
|
+
const filePaths = /* @__PURE__ */ new Map();
|
|
2993
|
+
const FileArgsSchema = z13.object({ path: z13.string() });
|
|
2840
2994
|
function handleTool(event) {
|
|
2841
2995
|
if (event.type === "tool_execution_start") {
|
|
2842
2996
|
flushText();
|
|
2843
2997
|
setActivity("thinking");
|
|
2998
|
+
if (event.toolName === "edit" || event.toolName === "write") {
|
|
2999
|
+
const args = FileArgsSchema.safeParse(event.args);
|
|
3000
|
+
if (args.success) filePaths.set(event.toolCallId, args.data.path);
|
|
3001
|
+
}
|
|
2844
3002
|
emit({
|
|
2845
3003
|
type: "tool.started",
|
|
2846
3004
|
sessionId,
|
|
2847
3005
|
toolCallId: event.toolCallId,
|
|
2848
3006
|
tool: event.toolName,
|
|
2849
|
-
input: boundedJson(event.args, 1200)
|
|
3007
|
+
input: boundedJson(event.args, 1200),
|
|
3008
|
+
filePath: filePaths.get(event.toolCallId)
|
|
2850
3009
|
});
|
|
2851
3010
|
return true;
|
|
2852
3011
|
}
|
|
@@ -2858,8 +3017,10 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2858
3017
|
toolCallId: event.toolCallId,
|
|
2859
3018
|
tool: event.toolName,
|
|
2860
3019
|
output: toolOutput(event.result),
|
|
3020
|
+
fileChange: event.isError ? void 0 : completedFileChange(event.result, filePaths.get(event.toolCallId)),
|
|
2861
3021
|
isError: event.isError
|
|
2862
3022
|
});
|
|
3023
|
+
filePaths.delete(event.toolCallId);
|
|
2863
3024
|
setActivity("thinking");
|
|
2864
3025
|
return true;
|
|
2865
3026
|
}
|
|
@@ -2888,9 +3049,9 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2888
3049
|
}
|
|
2889
3050
|
|
|
2890
3051
|
// src/projects.ts
|
|
2891
|
-
import { access, readdir as readdir2, realpath, stat as stat2, readFile as
|
|
3052
|
+
import { access, readdir as readdir2, realpath, stat as stat2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
2892
3053
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2893
|
-
import { z as
|
|
3054
|
+
import { z as z14 } from "zod";
|
|
2894
3055
|
import { constants } from "node:fs";
|
|
2895
3056
|
import { homedir } from "node:os";
|
|
2896
3057
|
import path6 from "node:path";
|
|
@@ -2922,12 +3083,12 @@ function projectLock(store2) {
|
|
|
2922
3083
|
}
|
|
2923
3084
|
return lock;
|
|
2924
3085
|
}
|
|
2925
|
-
var ProjectId =
|
|
3086
|
+
var ProjectId = z14.string().trim().min(1).max(255);
|
|
2926
3087
|
async function gitDirectory(directory) {
|
|
2927
3088
|
const location = path6.join(directory, ".git");
|
|
2928
3089
|
const info = await stat2(location);
|
|
2929
3090
|
if (info.isDirectory()) return location;
|
|
2930
|
-
const contents = await
|
|
3091
|
+
const contents = await readFile5(location, "utf8");
|
|
2931
3092
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
2932
3093
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
2933
3094
|
const resolved = await realpath(path6.resolve(directory, match[1].trim()));
|
|
@@ -2937,21 +3098,21 @@ async function gitDirectory(directory) {
|
|
|
2937
3098
|
async function identity(directory, preferred) {
|
|
2938
3099
|
const filename = path6.join(await gitDirectory(directory), "agentrific-project-id");
|
|
2939
3100
|
try {
|
|
2940
|
-
return ProjectId.parse(await
|
|
3101
|
+
return ProjectId.parse(await readFile5(filename, "utf8"));
|
|
2941
3102
|
} catch (error) {
|
|
2942
|
-
if (!
|
|
3103
|
+
if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
2943
3104
|
const legacyFilename = path6.join(await gitDirectory(directory), "chatplans-project-id");
|
|
2944
|
-
const legacy = await
|
|
2945
|
-
if (!
|
|
3105
|
+
const legacy = await readFile5(legacyFilename, "utf8").catch((cause) => {
|
|
3106
|
+
if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(cause).success) throw cause;
|
|
2946
3107
|
return null;
|
|
2947
3108
|
});
|
|
2948
3109
|
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
2949
3110
|
try {
|
|
2950
|
-
await
|
|
3111
|
+
await writeFile3(filename, id + "\n", { flag: "wx" });
|
|
2951
3112
|
return id;
|
|
2952
3113
|
} catch (writeError) {
|
|
2953
|
-
if (!
|
|
2954
|
-
return ProjectId.parse(await
|
|
3114
|
+
if (!z14.object({ code: z14.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
|
|
3115
|
+
return ProjectId.parse(await readFile5(filename, "utf8"));
|
|
2955
3116
|
}
|
|
2956
3117
|
}
|
|
2957
3118
|
}
|
|
@@ -2971,7 +3132,7 @@ function scanProjects(root, store2) {
|
|
|
2971
3132
|
let existing = registered.find((project2) => project2.id === id);
|
|
2972
3133
|
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat2(path6.join(existing.path, ".git")).catch(() => null)) {
|
|
2973
3134
|
id = randomUUID6();
|
|
2974
|
-
await
|
|
3135
|
+
await writeFile3(
|
|
2975
3136
|
path6.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
2976
3137
|
id + "\n"
|
|
2977
3138
|
);
|
|
@@ -3010,11 +3171,11 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3010
3171
|
const filename = path6.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3011
3172
|
const replacedId = await identity(directory);
|
|
3012
3173
|
const project = { id, name: path6.basename(directory), path: directory, available: true };
|
|
3013
|
-
await
|
|
3174
|
+
await writeFile3(filename, id + "\n");
|
|
3014
3175
|
try {
|
|
3015
3176
|
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
3016
3177
|
} catch (error) {
|
|
3017
|
-
await
|
|
3178
|
+
await writeFile3(filename, replacedId + "\n");
|
|
3018
3179
|
throw error;
|
|
3019
3180
|
}
|
|
3020
3181
|
return project;
|
|
@@ -3028,32 +3189,32 @@ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "n
|
|
|
3028
3189
|
import path7 from "node:path";
|
|
3029
3190
|
|
|
3030
3191
|
// src/push-schema.ts
|
|
3031
|
-
import { z as
|
|
3032
|
-
var PushBindingSchema =
|
|
3033
|
-
bindingId:
|
|
3034
|
-
installationId:
|
|
3035
|
-
deviceId:
|
|
3192
|
+
import { z as z15 } from "zod";
|
|
3193
|
+
var PushBindingSchema = z15.object({
|
|
3194
|
+
bindingId: z15.string().uuid(),
|
|
3195
|
+
installationId: z15.string().uuid(),
|
|
3196
|
+
deviceId: z15.string().uuid()
|
|
3036
3197
|
});
|
|
3037
|
-
var PushStateSchema =
|
|
3038
|
-
bindings:
|
|
3039
|
-
error:
|
|
3198
|
+
var PushStateSchema = z15.object({
|
|
3199
|
+
bindings: z15.array(PushBindingSchema).max(10),
|
|
3200
|
+
error: z15.string().nullable()
|
|
3040
3201
|
});
|
|
3041
3202
|
var PendingPushSchema = PushEventSchema.extend({
|
|
3042
|
-
remaining:
|
|
3043
|
-
createdAt:
|
|
3044
|
-
attempts:
|
|
3045
|
-
nextAt:
|
|
3203
|
+
remaining: z15.array(PushBindingSchema),
|
|
3204
|
+
createdAt: z15.number(),
|
|
3205
|
+
attempts: z15.number().int().nonnegative(),
|
|
3206
|
+
nextAt: z15.number()
|
|
3046
3207
|
});
|
|
3047
3208
|
|
|
3048
3209
|
// src/store.ts
|
|
3049
3210
|
import { DatabaseSync } from "node:sqlite";
|
|
3050
3211
|
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3051
|
-
import { z as
|
|
3212
|
+
import { z as z16 } from "zod";
|
|
3052
3213
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3053
3214
|
var StorageError = class extends Data3.TaggedError("StorageError") {
|
|
3054
3215
|
};
|
|
3055
|
-
var PayloadRow =
|
|
3056
|
-
var EventRow =
|
|
3216
|
+
var PayloadRow = z16.object({ payload: z16.string() });
|
|
3217
|
+
var EventRow = z16.object({ sequence: z16.number(), recorded_at: z16.string(), payload: z16.string() });
|
|
3057
3218
|
var SqliteSessionStore = class {
|
|
3058
3219
|
constructor(db) {
|
|
3059
3220
|
this.db = db;
|
|
@@ -3104,7 +3265,7 @@ var SqliteSessionStore = class {
|
|
|
3104
3265
|
const row = PayloadRow.parse(
|
|
3105
3266
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
3106
3267
|
);
|
|
3107
|
-
return
|
|
3268
|
+
return z16.string().uuid().parse(JSON.parse(row.payload));
|
|
3108
3269
|
});
|
|
3109
3270
|
}
|
|
3110
3271
|
saveDevice(device, tokenHash) {
|
|
@@ -3192,7 +3353,7 @@ var SqliteSessionStore = class {
|
|
|
3192
3353
|
`).all();
|
|
3193
3354
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
3194
3355
|
for (const row of legacy) {
|
|
3195
|
-
const id =
|
|
3356
|
+
const id = z16.object({ id: z16.string() }).parse(row).id;
|
|
3196
3357
|
insert.run(
|
|
3197
3358
|
id,
|
|
3198
3359
|
JSON.stringify({ id, name: id, path: path7.join(root, id), available: false })
|
|
@@ -3402,7 +3563,7 @@ var SqliteSessionStore = class {
|
|
|
3402
3563
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
3403
3564
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
3404
3565
|
if (notification) {
|
|
3405
|
-
const count =
|
|
3566
|
+
const count = z16.object({ total: z16.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
3406
3567
|
if (count.total >= 1e3)
|
|
3407
3568
|
this.db.prepare(
|
|
3408
3569
|
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
@@ -3454,7 +3615,7 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3454
3615
|
for (const suffix of ["-wal", "-shm"])
|
|
3455
3616
|
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
3456
3617
|
try {
|
|
3457
|
-
const schema =
|
|
3618
|
+
const schema = z16.object({ user_version: z16.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
3458
3619
|
if (schema.user_version > 1)
|
|
3459
3620
|
throw new Error("This database requires a newer Agentrific server.");
|
|
3460
3621
|
db.exec(`
|
|
@@ -3583,9 +3744,13 @@ var releaseLock = await acquireServerLock(dataRoot);
|
|
|
3583
3744
|
var effects = ManagedRuntime.make(SessionStore.layer(path9.join(dataRoot, "sessions.sqlite")));
|
|
3584
3745
|
var store = await effects.runPromise(SessionStore);
|
|
3585
3746
|
var security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
|
|
3586
|
-
var
|
|
3747
|
+
var testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
|
|
3748
|
+
if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
|
|
3749
|
+
throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
|
|
3750
|
+
var push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
|
|
3587
3751
|
var allowedOrigins = [
|
|
3588
3752
|
.../* @__PURE__ */ new Set([
|
|
3753
|
+
"agentrific-desktop://app",
|
|
3589
3754
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
3590
3755
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
3591
3756
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -3840,7 +4005,7 @@ app.get("/api/projects/:id", async (request, reply) => {
|
|
|
3840
4005
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
3841
4006
|
});
|
|
3842
4007
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
3843
|
-
const parsed =
|
|
4008
|
+
const parsed = z17.object({ path: z17.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
3844
4009
|
if (!parsed.success)
|
|
3845
4010
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
3846
4011
|
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
@@ -3972,9 +4137,9 @@ app.post(
|
|
|
3972
4137
|
}
|
|
3973
4138
|
})
|
|
3974
4139
|
);
|
|
3975
|
-
var HistoryQuery =
|
|
3976
|
-
after:
|
|
3977
|
-
limit:
|
|
4140
|
+
var HistoryQuery = z17.object({
|
|
4141
|
+
after: z17.coerce.number().int().nonnegative().default(0),
|
|
4142
|
+
limit: z17.coerce.number().int().min(1).max(1e3).default(500)
|
|
3978
4143
|
});
|
|
3979
4144
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
3980
4145
|
const query = HistoryQuery.safeParse(request.query);
|
|
@@ -4116,7 +4281,7 @@ app.post("/api/sessions", async (request, reply) => {
|
|
|
4116
4281
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
4117
4282
|
return reply.code(201).send(result.session);
|
|
4118
4283
|
});
|
|
4119
|
-
var StartTaskBody =
|
|
4284
|
+
var StartTaskBody = z17.object({ prompt: z17.string().trim().max(2e4).optional() }).default({});
|
|
4120
4285
|
function startTask(id, instructionsOverride, automated = false) {
|
|
4121
4286
|
return withTaskLock(
|
|
4122
4287
|
"agent-dispatch",
|
|
@@ -4194,7 +4359,7 @@ app.patch("/api/automation", async (request, reply) => {
|
|
|
4194
4359
|
return automation.configure(parsed.data);
|
|
4195
4360
|
});
|
|
4196
4361
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
4197
|
-
const parsed =
|
|
4362
|
+
const parsed = z17.object({ taskId: z17.string().min(1), beforeId: z17.string().min(1).nullable() }).safeParse(request.body);
|
|
4198
4363
|
if (!parsed.success)
|
|
4199
4364
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
4200
4365
|
try {
|
|
@@ -4224,7 +4389,13 @@ app.post(
|
|
|
4224
4389
|
attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
|
|
4225
4390
|
);
|
|
4226
4391
|
await Effect16.runPromise(
|
|
4227
|
-
runtime.send(
|
|
4392
|
+
runtime.send(
|
|
4393
|
+
session,
|
|
4394
|
+
parsed.data.message,
|
|
4395
|
+
files,
|
|
4396
|
+
parsed.data.clientMessageId,
|
|
4397
|
+
parsed.data.mode
|
|
4398
|
+
)
|
|
4228
4399
|
);
|
|
4229
4400
|
return reply.code(202).send({ accepted: true });
|
|
4230
4401
|
} catch (error) {
|