@agentrific/server 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/cli.js +80 -27
- package/cli.js.map +4 -4
- package/index.js +82 -35
- package/index.js.map +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.5.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Add a separate sandbox push service and require explicit test mode for sandbox grants so TestFlight purchases cannot unlock production notifications.
|
|
16
|
+
|
|
3
17
|
## 0.5.0
|
|
4
18
|
|
|
5
19
|
### Minor 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;
|
|
@@ -973,11 +1005,12 @@ function pushSigningText(installationId, operation, request) {
|
|
|
973
1005
|
request.payload
|
|
974
1006
|
].join("\n");
|
|
975
1007
|
}
|
|
976
|
-
var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
|
|
1008
|
+
var PUSH_API_URL, PUSH_SANDBOX_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
|
|
977
1009
|
var init_push = __esm({
|
|
978
1010
|
"../../packages/contracts/src/push.ts"() {
|
|
979
1011
|
"use strict";
|
|
980
1012
|
PUSH_API_URL = "https://api.agentrific.com";
|
|
1013
|
+
PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
|
|
981
1014
|
PushKindSchema = z6.enum([
|
|
982
1015
|
"started",
|
|
983
1016
|
"completed",
|
|
@@ -1541,7 +1574,7 @@ var init_event_server = __esm({
|
|
|
1541
1574
|
});
|
|
1542
1575
|
|
|
1543
1576
|
// src/security.ts
|
|
1544
|
-
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";
|
|
1545
1578
|
import { Effect as Effect5 } from "effect";
|
|
1546
1579
|
import { z as z8 } from "zod";
|
|
1547
1580
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
@@ -1629,7 +1662,7 @@ var init_security = __esm({
|
|
|
1629
1662
|
init_src();
|
|
1630
1663
|
init_pairing();
|
|
1631
1664
|
secret = () => randomBytes(32).toString("hex");
|
|
1632
|
-
digest = (value) =>
|
|
1665
|
+
digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
1633
1666
|
ServerSecurity = class {
|
|
1634
1667
|
constructor(store2, serverId) {
|
|
1635
1668
|
this.store = store2;
|
|
@@ -1755,7 +1788,7 @@ var init_maintenance = __esm({
|
|
|
1755
1788
|
this.enter();
|
|
1756
1789
|
const deadline = Date.now() + timeoutMs;
|
|
1757
1790
|
while (!this.status().drained && Date.now() < deadline)
|
|
1758
|
-
await new Promise((
|
|
1791
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
1759
1792
|
return this.status();
|
|
1760
1793
|
}
|
|
1761
1794
|
attach(app2) {
|
|
@@ -4182,7 +4215,7 @@ var init_store = __esm({
|
|
|
4182
4215
|
});
|
|
4183
4216
|
|
|
4184
4217
|
// src/worktree-cleanup.ts
|
|
4185
|
-
import { lstat, rm as rm2 } from "node:fs/promises";
|
|
4218
|
+
import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
|
|
4186
4219
|
import path10 from "node:path";
|
|
4187
4220
|
import { Effect as Effect15 } from "effect";
|
|
4188
4221
|
async function removeRunFiles(dataRoot2, sessionId) {
|
|
@@ -4208,7 +4241,7 @@ var init_worktree_cleanup = __esm({
|
|
|
4208
4241
|
async remove(session, discardChanges = true) {
|
|
4209
4242
|
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
4210
4243
|
const workspace = path10.resolve(session.workspacePath);
|
|
4211
|
-
const info = await
|
|
4244
|
+
const info = await lstat2(workspace).catch((error) => {
|
|
4212
4245
|
if (error.code === "ENOENT") return void 0;
|
|
4213
4246
|
throw error;
|
|
4214
4247
|
});
|
|
@@ -4512,10 +4545,15 @@ async function withSessionTaskLock(id, operation) {
|
|
|
4512
4545
|
const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
|
|
4513
4546
|
return withTaskLock(lockId, operation);
|
|
4514
4547
|
}
|
|
4515
|
-
|
|
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
|
+
}
|
|
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;
|
|
4516
4553
|
var init_index = __esm({
|
|
4517
4554
|
async "src/index.ts"() {
|
|
4518
4555
|
"use strict";
|
|
4556
|
+
init_push();
|
|
4519
4557
|
init_warnings();
|
|
4520
4558
|
init_push2();
|
|
4521
4559
|
init_attachments();
|
|
@@ -4571,13 +4609,23 @@ var init_index = __esm({
|
|
|
4571
4609
|
effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
|
|
4572
4610
|
store = await effects.runPromise(SessionStore);
|
|
4573
4611
|
security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
|
|
4612
|
+
sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
|
|
4613
|
+
if (sandboxPushUrl && process.env.AGENTRIFIC_TEST_MODE !== "1")
|
|
4614
|
+
throw new Error("Sandbox push requires explicit test mode.");
|
|
4574
4615
|
testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
|
|
4575
4616
|
if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
|
|
4576
4617
|
throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
|
|
4577
|
-
push = new ServerPush(
|
|
4618
|
+
push = new ServerPush(
|
|
4619
|
+
store,
|
|
4620
|
+
security.serverId,
|
|
4621
|
+
dataRoot,
|
|
4622
|
+
fetch,
|
|
4623
|
+
testPushUrl ?? sandboxPushUrl
|
|
4624
|
+
);
|
|
4578
4625
|
allowedOrigins = [
|
|
4579
4626
|
.../* @__PURE__ */ new Set([
|
|
4580
4627
|
"agentrific-desktop://app",
|
|
4628
|
+
"https://app.agentrific.com",
|
|
4581
4629
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
4582
4630
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
4583
4631
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -5110,6 +5158,7 @@ var init_index = __esm({
|
|
|
5110
5158
|
() => ({
|
|
5111
5159
|
serverId: security.serverId,
|
|
5112
5160
|
publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
|
|
5161
|
+
localUrl: localServerUrl(),
|
|
5113
5162
|
serverVersion,
|
|
5114
5163
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
5115
5164
|
connectedSockets: security.sockets.size,
|
|
@@ -5117,7 +5166,11 @@ var init_index = __esm({
|
|
|
5117
5166
|
activeRuns: activeAgents(Effect17.runSync(store.list())),
|
|
5118
5167
|
maintenance: maintenance.status()
|
|
5119
5168
|
}),
|
|
5120
|
-
maintenance
|
|
5169
|
+
maintenance,
|
|
5170
|
+
async () => {
|
|
5171
|
+
await app.close();
|
|
5172
|
+
process.exit(0);
|
|
5173
|
+
}
|
|
5121
5174
|
);
|
|
5122
5175
|
push.start();
|
|
5123
5176
|
app.addHook("onClose", async () => {
|
|
@@ -5285,7 +5338,7 @@ function signalChild(child, signal) {
|
|
|
5285
5338
|
}
|
|
5286
5339
|
}
|
|
5287
5340
|
function execute2(file, args, timeoutMs, env = process.env) {
|
|
5288
|
-
return new Promise((
|
|
5341
|
+
return new Promise((resolve2, reject) => {
|
|
5289
5342
|
const child = spawn(file, args, {
|
|
5290
5343
|
env,
|
|
5291
5344
|
detached: process.platform !== "win32",
|
|
@@ -5313,7 +5366,7 @@ function execute2(file, args, timeoutMs, env = process.env) {
|
|
|
5313
5366
|
child.once("close", (code) => {
|
|
5314
5367
|
clearTimeout(timeout);
|
|
5315
5368
|
if (code !== 0) reject(new Error("Command probe failed."));
|
|
5316
|
-
else
|
|
5369
|
+
else resolve2({ stdout, stderr });
|
|
5317
5370
|
});
|
|
5318
5371
|
function checkOutput() {
|
|
5319
5372
|
if (stdout.length + stderr.length <= 1048576) return;
|
|
@@ -5460,7 +5513,7 @@ var ManagedTunnel = class {
|
|
|
5460
5513
|
process.once("SIGINT", this.stopOnSignal);
|
|
5461
5514
|
process.once("SIGTERM", this.stopOnSignal);
|
|
5462
5515
|
process.once("exit", this.stopOnExit);
|
|
5463
|
-
return new Promise((
|
|
5516
|
+
return new Promise((resolve2, reject) => {
|
|
5464
5517
|
let output = "";
|
|
5465
5518
|
let address;
|
|
5466
5519
|
let settled = false;
|
|
@@ -5488,7 +5541,7 @@ var ManagedTunnel = class {
|
|
|
5488
5541
|
this.connected = true;
|
|
5489
5542
|
clearTimeout(timeout);
|
|
5490
5543
|
signal?.removeEventListener("abort", abort);
|
|
5491
|
-
|
|
5544
|
+
resolve2(address);
|
|
5492
5545
|
};
|
|
5493
5546
|
child.stdout?.on("data", read);
|
|
5494
5547
|
child.stderr?.on("data", read);
|
|
@@ -5519,11 +5572,11 @@ var ManagedTunnel = class {
|
|
|
5519
5572
|
const child = this.child;
|
|
5520
5573
|
if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
|
|
5521
5574
|
return Promise.resolve();
|
|
5522
|
-
this.closing ??= new Promise((
|
|
5575
|
+
this.closing ??= new Promise((resolve2) => {
|
|
5523
5576
|
const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
|
|
5524
5577
|
child.once("close", () => {
|
|
5525
5578
|
clearTimeout(timeout);
|
|
5526
|
-
|
|
5579
|
+
resolve2();
|
|
5527
5580
|
});
|
|
5528
5581
|
signalChild(child, "SIGTERM");
|
|
5529
5582
|
});
|