@agentrific/server 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/cli.js +66 -24
- package/cli.js.map +4 -4
- package/index.js +52 -13
- package/index.js.map +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @agentrific/server
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fe18d59: Add automatic updates for signed macOS desktop releases and an optional bundled local server that continues in the tray. Defer update installation and server shutdown while local agents are active. Host the web app and architecture-specific update feeds at app.agentrific.com.
|
|
8
|
+
|
|
9
|
+
Keep local control sockets usable with long data directory paths and allow the hosted web app to connect to paired servers.
|
|
10
|
+
|
|
3
11
|
## 0.5.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "@agentrific/server",
|
|
33
|
-
version: "0.5.
|
|
33
|
+
version: "0.5.2",
|
|
34
34
|
description: "Self-hosted control plane for long-running coding agents",
|
|
35
35
|
keywords: [
|
|
36
36
|
"agentrific",
|
|
@@ -203,6 +203,21 @@ var init_updates = __esm({
|
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
// ../../packages/contracts/src/control-socket.ts
|
|
207
|
+
import { createHash } from "node:crypto";
|
|
208
|
+
import { join, resolve } from "node:path";
|
|
209
|
+
function controlSocketPath(directory, uid = process.getuid?.()) {
|
|
210
|
+
const direct = join(resolve(directory), "control.sock");
|
|
211
|
+
if (Buffer.byteLength(direct) < 100) return direct;
|
|
212
|
+
if (uid === void 0) throw new Error("Local control sockets require macOS or Linux.");
|
|
213
|
+
const digest2 = createHash("sha256").update(resolve(directory)).digest("hex").slice(0, 32);
|
|
214
|
+
return join("/tmp", `agentrific-${uid}`, `${digest2}.sock`);
|
|
215
|
+
}
|
|
216
|
+
var init_control_socket = __esm({
|
|
217
|
+
"../../packages/contracts/src/control-socket.ts"() {
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
206
221
|
// ../../packages/contracts/src/index.ts
|
|
207
222
|
import { z as z2 } from "zod";
|
|
208
223
|
function cronRange(base, min, max, stepped) {
|
|
@@ -590,7 +605,7 @@ var init_src = __esm({
|
|
|
590
605
|
|
|
591
606
|
// src/control.ts
|
|
592
607
|
import { createServer, createConnection } from "node:net";
|
|
593
|
-
import { chmod, mkdir as mkdir2, readFile, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
608
|
+
import { chmod, mkdir as mkdir2, lstat, readFile, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
594
609
|
import path2 from "node:path";
|
|
595
610
|
import { Effect as Effect2 } from "effect";
|
|
596
611
|
import { z as z3 } from "zod";
|
|
@@ -635,8 +650,14 @@ async function acquireServerLock(directory) {
|
|
|
635
650
|
await unlink2(filename);
|
|
636
651
|
};
|
|
637
652
|
}
|
|
638
|
-
async function startControl(directory, security2, status, maintenance2) {
|
|
639
|
-
const filename =
|
|
653
|
+
async function startControl(directory, security2, status, maintenance2, shutdown) {
|
|
654
|
+
const filename = controlSocketPath(directory);
|
|
655
|
+
const socketDirectory = path2.dirname(filename);
|
|
656
|
+
await mkdir2(socketDirectory, { recursive: true, mode: 448 });
|
|
657
|
+
const metadata = await lstat(socketDirectory);
|
|
658
|
+
if (!metadata.isDirectory() || metadata.uid !== process.getuid?.())
|
|
659
|
+
throw new Error("Local control directory must be private and owned by this user.");
|
|
660
|
+
await chmod(socketDirectory, 448);
|
|
640
661
|
await unlink2(filename).catch((error) => {
|
|
641
662
|
if (!z3.object({ code: z3.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
642
663
|
});
|
|
@@ -656,6 +677,13 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
656
677
|
const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
|
|
657
678
|
let result;
|
|
658
679
|
switch (request.command) {
|
|
680
|
+
case "shutdown":
|
|
681
|
+
if (!shutdown || !maintenance2?.status().drained)
|
|
682
|
+
throw new Error("Drain the server before shutdown");
|
|
683
|
+
socket.end(JSON.stringify({ result: { shutdown: true } }) + "\n", () => {
|
|
684
|
+
setImmediate(() => void shutdown());
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
659
687
|
case "pair":
|
|
660
688
|
result = security2.createPairing();
|
|
661
689
|
break;
|
|
@@ -682,20 +710,20 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
682
710
|
});
|
|
683
711
|
socket.on("error", () => socket.destroy());
|
|
684
712
|
});
|
|
685
|
-
await new Promise((
|
|
713
|
+
await new Promise((resolve2, reject) => {
|
|
686
714
|
server.once("error", reject);
|
|
687
|
-
server.listen(filename,
|
|
715
|
+
server.listen(filename, resolve2);
|
|
688
716
|
});
|
|
689
717
|
await chmod(filename, 384);
|
|
690
718
|
return async () => {
|
|
691
719
|
await new Promise(
|
|
692
|
-
(
|
|
720
|
+
(resolve2, reject) => server.close((error) => error ? reject(error) : resolve2())
|
|
693
721
|
);
|
|
694
722
|
};
|
|
695
723
|
}
|
|
696
724
|
function controlRequest(directory, command) {
|
|
697
|
-
return new Promise((
|
|
698
|
-
const socket = createConnection(
|
|
725
|
+
return new Promise((resolve2, reject) => {
|
|
726
|
+
const socket = createConnection(controlSocketPath(directory));
|
|
699
727
|
socket.setTimeout(command.command === "maintenance" ? 65e3 : 5e3, () => {
|
|
700
728
|
socket.destroy();
|
|
701
729
|
reject(new Error("Server did not respond."));
|
|
@@ -711,7 +739,7 @@ function controlRequest(directory, command) {
|
|
|
711
739
|
const result = z3.object({ result: ControlResultSchema.optional(), error: z3.string().optional() }).parse(JSON.parse(response));
|
|
712
740
|
if (result.error) throw new Error(result.error);
|
|
713
741
|
if (!result.result) throw new Error("Server returned an empty response.");
|
|
714
|
-
|
|
742
|
+
resolve2(result.result);
|
|
715
743
|
} catch (error) {
|
|
716
744
|
reject(error);
|
|
717
745
|
}
|
|
@@ -722,6 +750,7 @@ var MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
|
|
|
722
750
|
var init_control = __esm({
|
|
723
751
|
"src/control.ts"() {
|
|
724
752
|
"use strict";
|
|
753
|
+
init_control_socket();
|
|
725
754
|
init_src();
|
|
726
755
|
MaintenanceSchema = z3.object({
|
|
727
756
|
active: z3.boolean(),
|
|
@@ -731,6 +760,7 @@ var init_control = __esm({
|
|
|
731
760
|
ControlStatusSchema = z3.object({
|
|
732
761
|
serverId: z3.string().uuid(),
|
|
733
762
|
publicUrl: z3.string().optional(),
|
|
763
|
+
localUrl: z3.string().optional(),
|
|
734
764
|
serverVersion: z3.string().optional(),
|
|
735
765
|
connectedDevices: z3.number().optional(),
|
|
736
766
|
connectedSockets: z3.number().optional(),
|
|
@@ -748,12 +778,14 @@ var init_control = __esm({
|
|
|
748
778
|
DeviceSchema.array(),
|
|
749
779
|
ControlStatusSchema,
|
|
750
780
|
z3.object({ revoked: z3.boolean() }),
|
|
781
|
+
z3.object({ shutdown: z3.literal(true) }),
|
|
751
782
|
MaintenanceSchema
|
|
752
783
|
]);
|
|
753
784
|
ControlRequest = z3.discriminatedUnion("command", [
|
|
754
785
|
z3.object({ command: z3.literal("pair") }),
|
|
755
786
|
z3.object({ command: z3.literal("devices") }),
|
|
756
787
|
z3.object({ command: z3.literal("status") }),
|
|
788
|
+
z3.object({ command: z3.literal("shutdown") }),
|
|
757
789
|
z3.object({ command: z3.literal("revoke"), id: z3.string().uuid() }),
|
|
758
790
|
z3.object({
|
|
759
791
|
command: z3.literal("maintenance"),
|
|
@@ -855,7 +887,7 @@ function printManualPairing(invitation) {
|
|
|
855
887
|
}
|
|
856
888
|
function waitForManualPairing(invitation) {
|
|
857
889
|
console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
|
|
858
|
-
return new Promise((
|
|
890
|
+
return new Promise((resolve2) => {
|
|
859
891
|
const input = process.stdin;
|
|
860
892
|
const previousRaw = input.isRaw;
|
|
861
893
|
const timeout = setTimeout(
|
|
@@ -875,7 +907,7 @@ function waitForManualPairing(invitation) {
|
|
|
875
907
|
process.removeListener("SIGTERM", terminated);
|
|
876
908
|
input.setRawMode(previousRaw);
|
|
877
909
|
input.pause();
|
|
878
|
-
|
|
910
|
+
resolve2();
|
|
879
911
|
}
|
|
880
912
|
function interrupted() {
|
|
881
913
|
process.exitCode = 130;
|
|
@@ -1542,7 +1574,7 @@ var init_event_server = __esm({
|
|
|
1542
1574
|
});
|
|
1543
1575
|
|
|
1544
1576
|
// src/security.ts
|
|
1545
|
-
import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
1577
|
+
import { createHash as createHash2, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
1546
1578
|
import { Effect as Effect5 } from "effect";
|
|
1547
1579
|
import { z as z8 } from "zod";
|
|
1548
1580
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
@@ -1630,7 +1662,7 @@ var init_security = __esm({
|
|
|
1630
1662
|
init_src();
|
|
1631
1663
|
init_pairing();
|
|
1632
1664
|
secret = () => randomBytes(32).toString("hex");
|
|
1633
|
-
digest = (value) =>
|
|
1665
|
+
digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
1634
1666
|
ServerSecurity = class {
|
|
1635
1667
|
constructor(store2, serverId) {
|
|
1636
1668
|
this.store = store2;
|
|
@@ -1756,7 +1788,7 @@ var init_maintenance = __esm({
|
|
|
1756
1788
|
this.enter();
|
|
1757
1789
|
const deadline = Date.now() + timeoutMs;
|
|
1758
1790
|
while (!this.status().drained && Date.now() < deadline)
|
|
1759
|
-
await new Promise((
|
|
1791
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
1760
1792
|
return this.status();
|
|
1761
1793
|
}
|
|
1762
1794
|
attach(app2) {
|
|
@@ -4183,7 +4215,7 @@ var init_store = __esm({
|
|
|
4183
4215
|
});
|
|
4184
4216
|
|
|
4185
4217
|
// src/worktree-cleanup.ts
|
|
4186
|
-
import { lstat, rm as rm2 } from "node:fs/promises";
|
|
4218
|
+
import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
|
|
4187
4219
|
import path10 from "node:path";
|
|
4188
4220
|
import { Effect as Effect15 } from "effect";
|
|
4189
4221
|
async function removeRunFiles(dataRoot2, sessionId) {
|
|
@@ -4209,7 +4241,7 @@ var init_worktree_cleanup = __esm({
|
|
|
4209
4241
|
async remove(session, discardChanges = true) {
|
|
4210
4242
|
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
4211
4243
|
const workspace = path10.resolve(session.workspacePath);
|
|
4212
|
-
const info = await
|
|
4244
|
+
const info = await lstat2(workspace).catch((error) => {
|
|
4213
4245
|
if (error.code === "ENOENT") return void 0;
|
|
4214
4246
|
throw error;
|
|
4215
4247
|
});
|
|
@@ -4513,6 +4545,10 @@ async function withSessionTaskLock(id, operation) {
|
|
|
4513
4545
|
const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
|
|
4514
4546
|
return withTaskLock(lockId, operation);
|
|
4515
4547
|
}
|
|
4548
|
+
function localServerUrl() {
|
|
4549
|
+
const address = z18.object({ port: z18.number() }).safeParse(app.server.address());
|
|
4550
|
+
return address.success ? `http://${host}:${address.data.port}` : void 0;
|
|
4551
|
+
}
|
|
4516
4552
|
var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
|
|
4517
4553
|
var init_index = __esm({
|
|
4518
4554
|
async "src/index.ts"() {
|
|
@@ -4589,6 +4625,7 @@ var init_index = __esm({
|
|
|
4589
4625
|
allowedOrigins = [
|
|
4590
4626
|
.../* @__PURE__ */ new Set([
|
|
4591
4627
|
"agentrific-desktop://app",
|
|
4628
|
+
"https://app.agentrific.com",
|
|
4592
4629
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
4593
4630
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
4594
4631
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -5121,6 +5158,7 @@ var init_index = __esm({
|
|
|
5121
5158
|
() => ({
|
|
5122
5159
|
serverId: security.serverId,
|
|
5123
5160
|
publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
|
|
5161
|
+
localUrl: localServerUrl(),
|
|
5124
5162
|
serverVersion,
|
|
5125
5163
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
5126
5164
|
connectedSockets: security.sockets.size,
|
|
@@ -5128,7 +5166,11 @@ var init_index = __esm({
|
|
|
5128
5166
|
activeRuns: activeAgents(Effect17.runSync(store.list())),
|
|
5129
5167
|
maintenance: maintenance.status()
|
|
5130
5168
|
}),
|
|
5131
|
-
maintenance
|
|
5169
|
+
maintenance,
|
|
5170
|
+
async () => {
|
|
5171
|
+
await app.close();
|
|
5172
|
+
process.exit(0);
|
|
5173
|
+
}
|
|
5132
5174
|
);
|
|
5133
5175
|
push.start();
|
|
5134
5176
|
app.addHook("onClose", async () => {
|
|
@@ -5296,7 +5338,7 @@ function signalChild(child, signal) {
|
|
|
5296
5338
|
}
|
|
5297
5339
|
}
|
|
5298
5340
|
function execute2(file, args, timeoutMs, env = process.env) {
|
|
5299
|
-
return new Promise((
|
|
5341
|
+
return new Promise((resolve2, reject) => {
|
|
5300
5342
|
const child = spawn(file, args, {
|
|
5301
5343
|
env,
|
|
5302
5344
|
detached: process.platform !== "win32",
|
|
@@ -5324,7 +5366,7 @@ function execute2(file, args, timeoutMs, env = process.env) {
|
|
|
5324
5366
|
child.once("close", (code) => {
|
|
5325
5367
|
clearTimeout(timeout);
|
|
5326
5368
|
if (code !== 0) reject(new Error("Command probe failed."));
|
|
5327
|
-
else
|
|
5369
|
+
else resolve2({ stdout, stderr });
|
|
5328
5370
|
});
|
|
5329
5371
|
function checkOutput() {
|
|
5330
5372
|
if (stdout.length + stderr.length <= 1048576) return;
|
|
@@ -5471,7 +5513,7 @@ var ManagedTunnel = class {
|
|
|
5471
5513
|
process.once("SIGINT", this.stopOnSignal);
|
|
5472
5514
|
process.once("SIGTERM", this.stopOnSignal);
|
|
5473
5515
|
process.once("exit", this.stopOnExit);
|
|
5474
|
-
return new Promise((
|
|
5516
|
+
return new Promise((resolve2, reject) => {
|
|
5475
5517
|
let output = "";
|
|
5476
5518
|
let address;
|
|
5477
5519
|
let settled = false;
|
|
@@ -5499,7 +5541,7 @@ var ManagedTunnel = class {
|
|
|
5499
5541
|
this.connected = true;
|
|
5500
5542
|
clearTimeout(timeout);
|
|
5501
5543
|
signal?.removeEventListener("abort", abort);
|
|
5502
|
-
|
|
5544
|
+
resolve2(address);
|
|
5503
5545
|
};
|
|
5504
5546
|
child.stdout?.on("data", read);
|
|
5505
5547
|
child.stderr?.on("data", read);
|
|
@@ -5530,11 +5572,11 @@ var ManagedTunnel = class {
|
|
|
5530
5572
|
const child = this.child;
|
|
5531
5573
|
if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
|
|
5532
5574
|
return Promise.resolve();
|
|
5533
|
-
this.closing ??= new Promise((
|
|
5575
|
+
this.closing ??= new Promise((resolve2) => {
|
|
5534
5576
|
const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
|
|
5535
5577
|
child.once("close", () => {
|
|
5536
5578
|
clearTimeout(timeout);
|
|
5537
|
-
|
|
5579
|
+
resolve2();
|
|
5538
5580
|
});
|
|
5539
5581
|
signalChild(child, "SIGTERM");
|
|
5540
5582
|
});
|