@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @agentrific/server
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 70ffb32: Add an Electron desktop client with encrypted pairing storage and task notifications, macOS preview and signed release builds, and a marketing downloads page backed by public Cloudflare R2 artifacts. Allow the desktop app origin in the server's default origin policy.
|
|
8
|
+
- 5e2d3a2: Show task titles and event-specific notification details, open validated GitHub pull requests from PR alerts, and provide transparent Android notification and adaptive/themed launcher icons. Native icon configuration requires a rebuilt client and a new release runtime. Add an explicitly isolated, expiring local push test mode without weakening production billing checks.
|
|
9
|
+
- 991dd7e: Add a Pair device modal to web Settings with a locally generated QR code, separate six-digit PIN, expiry countdown and retry handling. Authenticated clients can request a single-use invitation from the connected server using its configured public HTTPS URL.
|
|
10
|
+
|
|
3
11
|
## 0.1.5
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ The server defaults to port 4318 and stores its configuration and state in `~/.a
|
|
|
32
32
|
|
|
33
33
|
Open the Agentrific app on iOS or Android. In the server terminal, choose Tailscale Serve, Cloudflare Tunnel, or an existing HTTPS endpoint, then press **p** to display a pairing QR code. Scan it in the app and enter the separately printed six-digit PIN.
|
|
34
34
|
|
|
35
|
+
Already paired in the web app? Open **Settings → Pair device** to generate a QR code and separate PIN for another device. Invitations expire after five minutes and can be used once. The server must have a public HTTPS URL configured (`AGENTRIFIC_PUBLIC_URL`); restart after changing it. Only share invitations with trusted devices: pairing grants access to run agents as the server’s host user.
|
|
36
|
+
|
|
35
37
|
Once paired, select a project, create a task and start an agent. The server runs the work and streams progress back to your device. Use HTTPS for remote connections; your phone must be able to reach the configured server address.
|
|
36
38
|
|
|
37
39
|
See the [server setup and pairing guide](https://agentrific.com/docs/secure-server-setup) for connection options and configuration.
|
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.1.
|
|
33
|
+
version: "0.1.6",
|
|
34
34
|
description: "Self-hosted control plane for long-running coding agents",
|
|
35
35
|
keywords: [
|
|
36
36
|
"agentrific",
|
|
@@ -245,7 +245,7 @@ function createPairingLink(payload) {
|
|
|
245
245
|
link.searchParams.set("data", JSON.stringify(PairingPayloadSchema.parse(payload)));
|
|
246
246
|
return link.toString();
|
|
247
247
|
}
|
|
248
|
-
var cronBounds, AutomationConfigSchema, AutomationStateSchema, SessionStatusSchema, AgentKindSchema, ModelReferenceSchema, ModelIdentitySchema, ModelOptionSchema, ModelCatalogSchema, GitFlowSchema, GitDeliverySchema, DeliveryRequestSchema, RunPreparationSchema, SessionSchema, MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, AttachmentSchema, AttachmentIdsSchema, CreateSessionSchema, SendMessageSchema, SessionEventSchema, RecordedEventSchema, EventHistorySchema, ServerEventSchema, ServerSettingsSchema, ConnectionsStatusSchema, UpdateSettingsSchema, ProjectSchema, TaskStatusSchema, TaskSchema, CreateTaskSchema, UpdateTaskSchema, PROTOCOL_VERSION, ServerUpdateStatusSchema, ServerHealthSchema, PairingPayloadSchema, PairingRequestSchema, DeviceSchema, PairingResultSchema, HandshakeSchema, HandshakeAckSchema;
|
|
248
|
+
var cronBounds, AutomationConfigSchema, AutomationStateSchema, SessionStatusSchema, AgentKindSchema, ModelReferenceSchema, ModelIdentitySchema, ModelOptionSchema, ModelCatalogSchema, GitFlowSchema, GitDeliverySchema, DeliveryRequestSchema, RunPreparationSchema, SessionSchema, MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, AttachmentSchema, AttachmentIdsSchema, CreateSessionSchema, SendMessageSchema, SessionEventSchema, RecordedEventSchema, EventHistorySchema, ServerEventSchema, ServerSettingsSchema, ConnectionsStatusSchema, UpdateSettingsSchema, ProjectSchema, TaskStatusSchema, TaskSchema, CreateTaskSchema, UpdateTaskSchema, PROTOCOL_VERSION, ServerUpdateStatusSchema, ServerHealthSchema, PairingPayloadSchema, PairingInvitationSchema, PairingRequestSchema, DeviceSchema, PairingResultSchema, HandshakeSchema, HandshakeAckSchema;
|
|
249
249
|
var init_src = __esm({
|
|
250
250
|
"../../packages/contracts/src/index.ts"() {
|
|
251
251
|
cronBounds = [
|
|
@@ -519,6 +519,11 @@ var init_src = __esm({
|
|
|
519
519
|
serverId: z2.string().uuid(),
|
|
520
520
|
pairingId: z2.string().regex(/^[a-f0-9]{64}$/)
|
|
521
521
|
});
|
|
522
|
+
PairingInvitationSchema = z2.object({
|
|
523
|
+
payload: PairingPayloadSchema,
|
|
524
|
+
pin: z2.string().regex(/^\d{6}$/),
|
|
525
|
+
expiresAt: z2.number()
|
|
526
|
+
});
|
|
522
527
|
PairingRequestSchema = z2.object({
|
|
523
528
|
pairingId: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
524
529
|
pin: z2.string().regex(/^\d{6}$/),
|
|
@@ -727,6 +732,202 @@ var init_control = __esm({
|
|
|
727
732
|
}
|
|
728
733
|
});
|
|
729
734
|
|
|
735
|
+
// src/cli-output.ts
|
|
736
|
+
import { styleText } from "node:util";
|
|
737
|
+
function printHeading(title) {
|
|
738
|
+
console.log();
|
|
739
|
+
console.log(styleText("bold", " ####### ##"));
|
|
740
|
+
console.log(styleText("bold", " ### #####"));
|
|
741
|
+
console.log(` ${styleText("bold", "## ####")} ${styleText("bold", "AGENTRIFIC")}`);
|
|
742
|
+
console.log(
|
|
743
|
+
` ${styleText("bold", "## ")}${styleText(["bold", "yellow"], "o")}${styleText("bold", " ####")} ${styleText("dim", `${title} \xB7 v${serverVersion}`)}`
|
|
744
|
+
);
|
|
745
|
+
console.log(styleText("bold", " ## ####"));
|
|
746
|
+
console.log(styleText("bold", " ### #####"));
|
|
747
|
+
console.log(styleText("bold", " ####### ##"));
|
|
748
|
+
console.log();
|
|
749
|
+
}
|
|
750
|
+
function printDetail(label2, value) {
|
|
751
|
+
console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
|
|
752
|
+
}
|
|
753
|
+
function printShortcuts(pairingAvailable = false) {
|
|
754
|
+
console.log();
|
|
755
|
+
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "p")} \u2502 pair a device`);
|
|
756
|
+
if (pairingAvailable)
|
|
757
|
+
console.log(
|
|
758
|
+
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "j")} \u2502 show pairing JSON`
|
|
759
|
+
);
|
|
760
|
+
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
|
|
761
|
+
console.log(
|
|
762
|
+
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
|
|
763
|
+
`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
function printError(message) {
|
|
767
|
+
console.error(`
|
|
768
|
+
${styleText(["bold", "red"], "Error")} ${message}
|
|
769
|
+
`);
|
|
770
|
+
}
|
|
771
|
+
function printHelp() {
|
|
772
|
+
printHeading("server");
|
|
773
|
+
console.log(` ${styleText("bold", "Usage")} agentrific <command> [options]
|
|
774
|
+
`);
|
|
775
|
+
for (const [command, description] of [
|
|
776
|
+
["init", "Configure the server"],
|
|
777
|
+
["start", "Start the server \xB7 press p to pair"],
|
|
778
|
+
["pair", "Print a fresh pairing QR code and PIN"],
|
|
779
|
+
["devices", "List enrolled devices"],
|
|
780
|
+
["revoke <id>", "Revoke a device's access"],
|
|
781
|
+
["status", "Show server status"],
|
|
782
|
+
["maintenance enter/resume/drain", "Pause new work, resume, or wait for work to finish"],
|
|
783
|
+
["service install/uninstall/restart", "Manage the background service"],
|
|
784
|
+
["logs", "Follow server logs"],
|
|
785
|
+
["update --check", "Check for a newer release"]
|
|
786
|
+
])
|
|
787
|
+
console.log(` ${styleText("cyan", command.padEnd(34))}${description}`);
|
|
788
|
+
console.log(`
|
|
789
|
+
${styleText("bold", "Options")}
|
|
790
|
+
`);
|
|
791
|
+
for (const [option, description] of [
|
|
792
|
+
["--data-dir <path>", "Use a specific server data directory"],
|
|
793
|
+
["--public-url <https-url>", "Use your tunnel URL without prompting"],
|
|
794
|
+
["--no-interactive", "Disable prompts and keyboard shortcuts"],
|
|
795
|
+
["--show-payload", "Print manual JSON with the pairing QR code"],
|
|
796
|
+
["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
|
|
797
|
+
["--version", "Print the server version"],
|
|
798
|
+
["--help", "Show this help"]
|
|
799
|
+
])
|
|
800
|
+
console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
|
|
801
|
+
console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
|
|
802
|
+
}
|
|
803
|
+
var init_cli_output = __esm({
|
|
804
|
+
"src/cli-output.ts"() {
|
|
805
|
+
"use strict";
|
|
806
|
+
init_version();
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// src/pairing.ts
|
|
811
|
+
import { z as z4 } from "zod";
|
|
812
|
+
import { styleText as styleText2 } from "node:util";
|
|
813
|
+
import { emitKeypressEvents } from "node:readline";
|
|
814
|
+
import qrcode from "qrcode-terminal";
|
|
815
|
+
function printManualPairing(invitation) {
|
|
816
|
+
if (Date.now() >= invitation.expiresAt) {
|
|
817
|
+
printError("This invitation expired. Generate a fresh pairing QR code.");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
console.log(styleText2("dim", "\n Manual entry \xB7 paste this JSON into Pairing payload:\n"));
|
|
821
|
+
console.log(` ${invitation.payload}
|
|
822
|
+
`);
|
|
823
|
+
}
|
|
824
|
+
function waitForManualPairing(invitation) {
|
|
825
|
+
console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
|
|
826
|
+
return new Promise((resolve) => {
|
|
827
|
+
const input = process.stdin;
|
|
828
|
+
const previousRaw = input.isRaw;
|
|
829
|
+
const timeout = setTimeout(
|
|
830
|
+
() => {
|
|
831
|
+
console.log(
|
|
832
|
+
styleText2("dim", "\n Pairing invitation expired. Run agentrific pair again.\n")
|
|
833
|
+
);
|
|
834
|
+
close();
|
|
835
|
+
},
|
|
836
|
+
Math.max(0, invitation.expiresAt - Date.now())
|
|
837
|
+
);
|
|
838
|
+
function close() {
|
|
839
|
+
clearTimeout(timeout);
|
|
840
|
+
input.removeListener("keypress", pressed);
|
|
841
|
+
input.removeListener("end", close);
|
|
842
|
+
process.removeListener("SIGINT", interrupted);
|
|
843
|
+
process.removeListener("SIGTERM", terminated);
|
|
844
|
+
input.setRawMode(previousRaw);
|
|
845
|
+
input.pause();
|
|
846
|
+
resolve();
|
|
847
|
+
}
|
|
848
|
+
function interrupted() {
|
|
849
|
+
process.exitCode = 130;
|
|
850
|
+
close();
|
|
851
|
+
}
|
|
852
|
+
function terminated() {
|
|
853
|
+
process.exitCode = 143;
|
|
854
|
+
close();
|
|
855
|
+
}
|
|
856
|
+
function pressed(text, key) {
|
|
857
|
+
if (key.ctrl && key.name === "c") {
|
|
858
|
+
interrupted();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (key.ctrl || key.meta) return;
|
|
862
|
+
if (text?.toLowerCase() === "j") {
|
|
863
|
+
printManualPairing(invitation);
|
|
864
|
+
close();
|
|
865
|
+
} else if (text?.toLowerCase() === "q" || key.name === "return" || key.name === "escape") {
|
|
866
|
+
close();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
emitKeypressEvents(input);
|
|
870
|
+
input.setRawMode(true);
|
|
871
|
+
input.on("keypress", pressed);
|
|
872
|
+
input.once("end", close);
|
|
873
|
+
process.once("SIGINT", interrupted);
|
|
874
|
+
process.once("SIGTERM", terminated);
|
|
875
|
+
input.resume();
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function pairingOrigin(url) {
|
|
879
|
+
const endpoint = new URL(url);
|
|
880
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
881
|
+
throw new Error("Use the HTTPS server origin without a path or credentials.");
|
|
882
|
+
if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
|
|
883
|
+
throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
|
|
884
|
+
return endpoint.origin;
|
|
885
|
+
}
|
|
886
|
+
async function printPairing(dataDir, url) {
|
|
887
|
+
if (!url)
|
|
888
|
+
throw new Error(
|
|
889
|
+
"Specify --public-url with your Tailscale Serve or Cloudflare Tunnel HTTPS address."
|
|
890
|
+
);
|
|
891
|
+
const origin = pairingOrigin(url);
|
|
892
|
+
const grant = z4.object({
|
|
893
|
+
pairingId: z4.string(),
|
|
894
|
+
pin: z4.string(),
|
|
895
|
+
expiresAt: z4.number(),
|
|
896
|
+
serverId: z4.string()
|
|
897
|
+
}).parse(await controlRequest(dataDir, { command: "pair" }));
|
|
898
|
+
const invitation = {
|
|
899
|
+
v: 1,
|
|
900
|
+
url: origin,
|
|
901
|
+
serverId: grant.serverId,
|
|
902
|
+
pairingId: grant.pairingId
|
|
903
|
+
};
|
|
904
|
+
const payload = JSON.stringify(invitation);
|
|
905
|
+
const link = createPairingLink(invitation);
|
|
906
|
+
printHeading("pairing");
|
|
907
|
+
console.log(" 1. Scan below with your phone camera and tap Open Agentrific if offered.");
|
|
908
|
+
console.log(" 2. Or open Agentrific and choose Scan pairing QR code.");
|
|
909
|
+
console.log(" 3. Enter the PIN, then confirm pairing.\n");
|
|
910
|
+
qrcode.generate(link, { small: true }, (code) => {
|
|
911
|
+
console.log(
|
|
912
|
+
code.split("\n").map((line) => ` ${line}`).join("\n")
|
|
913
|
+
);
|
|
914
|
+
});
|
|
915
|
+
console.log();
|
|
916
|
+
printDetail("PIN:", styleText2(["bold", "yellow"], grant.pin));
|
|
917
|
+
printDetail("Server", styleText2("cyan", origin));
|
|
918
|
+
printDetail("Expires", "In five minutes \xB7 one device per code");
|
|
919
|
+
console.log("\n Pairing grants this device access to run agents as your host user.\n");
|
|
920
|
+
return { payload, expiresAt: grant.expiresAt };
|
|
921
|
+
}
|
|
922
|
+
var init_pairing = __esm({
|
|
923
|
+
"src/pairing.ts"() {
|
|
924
|
+
"use strict";
|
|
925
|
+
init_src();
|
|
926
|
+
init_control();
|
|
927
|
+
init_cli_output();
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
730
931
|
// ../../packages/contracts/src/push.ts
|
|
731
932
|
import { z as z6 } from "zod";
|
|
732
933
|
function pushSigningText(installationId, operation, request) {
|
|
@@ -740,7 +941,7 @@ function pushSigningText(installationId, operation, request) {
|
|
|
740
941
|
request.payload
|
|
741
942
|
].join("\n");
|
|
742
943
|
}
|
|
743
|
-
var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
|
|
944
|
+
var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
|
|
744
945
|
var init_push = __esm({
|
|
745
946
|
"../../packages/contracts/src/push.ts"() {
|
|
746
947
|
"use strict";
|
|
@@ -768,7 +969,14 @@ var init_push = __esm({
|
|
|
768
969
|
grant: z6.string().regex(/^[a-f0-9]{64}$/),
|
|
769
970
|
expiresAt: z6.number()
|
|
770
971
|
}).strict();
|
|
771
|
-
|
|
972
|
+
PushPullRequestUrlSchema = z6.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
|
|
973
|
+
PushEventSchema = z6.object({
|
|
974
|
+
eventId: z6.string().uuid(),
|
|
975
|
+
sessionId: z6.string().uuid(),
|
|
976
|
+
kind: PushKindSchema,
|
|
977
|
+
taskTitle: z6.string().trim().min(1).max(160).optional(),
|
|
978
|
+
prUrl: PushPullRequestUrlSchema.optional()
|
|
979
|
+
}).strict();
|
|
772
980
|
PushSignedRequestSchema = z6.object({
|
|
773
981
|
bindingId: z6.string().uuid(),
|
|
774
982
|
timestamp: z6.number().int(),
|
|
@@ -855,10 +1063,11 @@ var init_push2 = __esm({
|
|
|
855
1063
|
"use strict";
|
|
856
1064
|
init_push();
|
|
857
1065
|
ServerPush = class {
|
|
858
|
-
constructor(store2, serverId, directory, transport = fetch) {
|
|
1066
|
+
constructor(store2, serverId, directory, transport = fetch, apiUrl = PUSH_API_URL) {
|
|
859
1067
|
this.store = store2;
|
|
860
1068
|
this.serverId = serverId;
|
|
861
1069
|
this.transport = transport;
|
|
1070
|
+
this.apiUrl = apiUrl;
|
|
862
1071
|
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
863
1072
|
const filename = path4.join(directory, "push-signing-key.pem");
|
|
864
1073
|
if (!existsSync(filename))
|
|
@@ -873,6 +1082,7 @@ var init_push2 = __esm({
|
|
|
873
1082
|
store;
|
|
874
1083
|
serverId;
|
|
875
1084
|
transport;
|
|
1085
|
+
apiUrl;
|
|
876
1086
|
key;
|
|
877
1087
|
publicKey;
|
|
878
1088
|
draining;
|
|
@@ -886,6 +1096,8 @@ var init_push2 = __esm({
|
|
|
886
1096
|
eventId: randomUUID(),
|
|
887
1097
|
sessionId: session.id,
|
|
888
1098
|
kind,
|
|
1099
|
+
taskTitle: session.title.replace(/[\r\n\t]+/g, " ").trim().slice(0, 160) || void 0,
|
|
1100
|
+
prUrl: kind === "pr_created" ? PushPullRequestUrlSchema.safeParse(session.gitDelivery?.url).data : void 0,
|
|
889
1101
|
remaining: state.bindings,
|
|
890
1102
|
createdAt: Date.now(),
|
|
891
1103
|
attempts: 0,
|
|
@@ -908,7 +1120,7 @@ var init_push2 = __esm({
|
|
|
908
1120
|
Buffer.from(pushSigningText(installationId, operation, request)),
|
|
909
1121
|
this.key
|
|
910
1122
|
).toString("hex");
|
|
911
|
-
return this.transport(`${
|
|
1123
|
+
return this.transport(`${this.apiUrl}/v1/installations/${installationId}/${operation}`, {
|
|
912
1124
|
method: "POST",
|
|
913
1125
|
headers: { "Content-Type": "application/json" },
|
|
914
1126
|
body: JSON.stringify({ ...request, signature }),
|
|
@@ -982,7 +1194,13 @@ var init_push2 = __esm({
|
|
|
982
1194
|
binding.installationId,
|
|
983
1195
|
binding.bindingId,
|
|
984
1196
|
"events",
|
|
985
|
-
JSON.stringify({
|
|
1197
|
+
JSON.stringify({
|
|
1198
|
+
eventId: job.eventId,
|
|
1199
|
+
sessionId: job.sessionId,
|
|
1200
|
+
kind: job.kind,
|
|
1201
|
+
taskTitle: job.taskTitle,
|
|
1202
|
+
prUrl: job.prUrl
|
|
1203
|
+
})
|
|
986
1204
|
);
|
|
987
1205
|
if (response.ok) continue;
|
|
988
1206
|
if ([401, 403, 404].includes(response.status)) {
|
|
@@ -1332,6 +1550,29 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1332
1550
|
const result = await security2.pair(input.data, request.ip);
|
|
1333
1551
|
return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
|
|
1334
1552
|
});
|
|
1553
|
+
app2.post("/api/auth/pairing-invitation", (_request, reply) => {
|
|
1554
|
+
const publicUrl = process.env.AGENTRIFIC_PUBLIC_URL;
|
|
1555
|
+
if (!publicUrl)
|
|
1556
|
+
return reply.code(503).send({
|
|
1557
|
+
error: "Configure the server\u2019s public HTTPS URL and restart the server before pairing a device."
|
|
1558
|
+
});
|
|
1559
|
+
let url;
|
|
1560
|
+
try {
|
|
1561
|
+
url = pairingOrigin(publicUrl);
|
|
1562
|
+
} catch {
|
|
1563
|
+
return reply.code(503).send({ error: "The server\u2019s public URL is invalid. Configure a public HTTPS origin." });
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const grant = security2.createPairing();
|
|
1567
|
+
return {
|
|
1568
|
+
payload: { v: 1, url, serverId: grant.serverId, pairingId: grant.pairingId },
|
|
1569
|
+
pin: grant.pin,
|
|
1570
|
+
expiresAt: grant.expiresAt
|
|
1571
|
+
};
|
|
1572
|
+
} catch {
|
|
1573
|
+
return reply.code(429).send({ error: "Too many pending pairing codes. Wait five minutes and try again." });
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1335
1576
|
app2.get("/api/auth/session", (request) => ({
|
|
1336
1577
|
serverId: security2.serverId,
|
|
1337
1578
|
device: security2.authenticate((request.headers.authorization ?? "").slice(7))
|
|
@@ -1354,6 +1595,7 @@ var init_security = __esm({
|
|
|
1354
1595
|
"src/security.ts"() {
|
|
1355
1596
|
"use strict";
|
|
1356
1597
|
init_src();
|
|
1598
|
+
init_pairing();
|
|
1357
1599
|
secret = () => randomBytes(32).toString("hex");
|
|
1358
1600
|
digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
1359
1601
|
ServerSecurity = class {
|
|
@@ -3968,7 +4210,7 @@ async function withSessionTaskLock(id, operation) {
|
|
|
3968
4210
|
const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
|
|
3969
4211
|
return withTaskLock(lockId, operation);
|
|
3970
4212
|
}
|
|
3971
|
-
var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
|
|
4213
|
+
var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
|
|
3972
4214
|
var init_index = __esm({
|
|
3973
4215
|
async "src/index.ts"() {
|
|
3974
4216
|
"use strict";
|
|
@@ -4025,9 +4267,13 @@ var init_index = __esm({
|
|
|
4025
4267
|
effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
|
|
4026
4268
|
store = await effects.runPromise(SessionStore);
|
|
4027
4269
|
security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
|
|
4028
|
-
|
|
4270
|
+
testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
|
|
4271
|
+
if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
|
|
4272
|
+
throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
|
|
4273
|
+
push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
|
|
4029
4274
|
allowedOrigins = [
|
|
4030
4275
|
.../* @__PURE__ */ new Set([
|
|
4276
|
+
"agentrific-desktop://app",
|
|
4031
4277
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
4032
4278
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
4033
4279
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -4635,204 +4881,21 @@ async function installService(platform, directory, filename, domain, dataDir, cl
|
|
|
4635
4881
|
init_updates();
|
|
4636
4882
|
init_control();
|
|
4637
4883
|
init_version();
|
|
4884
|
+
init_pairing();
|
|
4638
4885
|
import { spawn as spawn2 } from "node:child_process";
|
|
4639
4886
|
import { parseArgs } from "node:util";
|
|
4640
4887
|
import { z as z18 } from "zod";
|
|
4641
4888
|
|
|
4642
|
-
// src/pairing.ts
|
|
4643
|
-
init_src();
|
|
4644
|
-
init_control();
|
|
4645
|
-
import { z as z4 } from "zod";
|
|
4646
|
-
import { styleText as styleText2 } from "node:util";
|
|
4647
|
-
import { emitKeypressEvents } from "node:readline";
|
|
4648
|
-
import qrcode from "qrcode-terminal";
|
|
4649
|
-
|
|
4650
|
-
// src/cli-output.ts
|
|
4651
|
-
init_version();
|
|
4652
|
-
import { styleText } from "node:util";
|
|
4653
|
-
function printHeading(title) {
|
|
4654
|
-
console.log();
|
|
4655
|
-
console.log(styleText("bold", " ####### ##"));
|
|
4656
|
-
console.log(styleText("bold", " ### #####"));
|
|
4657
|
-
console.log(` ${styleText("bold", "## ####")} ${styleText("bold", "AGENTRIFIC")}`);
|
|
4658
|
-
console.log(
|
|
4659
|
-
` ${styleText("bold", "## ")}${styleText(["bold", "yellow"], "o")}${styleText("bold", " ####")} ${styleText("dim", `${title} \xB7 v${serverVersion}`)}`
|
|
4660
|
-
);
|
|
4661
|
-
console.log(styleText("bold", " ## ####"));
|
|
4662
|
-
console.log(styleText("bold", " ### #####"));
|
|
4663
|
-
console.log(styleText("bold", " ####### ##"));
|
|
4664
|
-
console.log();
|
|
4665
|
-
}
|
|
4666
|
-
function printDetail(label2, value) {
|
|
4667
|
-
console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
|
|
4668
|
-
}
|
|
4669
|
-
function printShortcuts(pairingAvailable = false) {
|
|
4670
|
-
console.log();
|
|
4671
|
-
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "p")} \u2502 pair a device`);
|
|
4672
|
-
if (pairingAvailable)
|
|
4673
|
-
console.log(
|
|
4674
|
-
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "j")} \u2502 show pairing JSON`
|
|
4675
|
-
);
|
|
4676
|
-
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
|
|
4677
|
-
console.log(
|
|
4678
|
-
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
|
|
4679
|
-
`
|
|
4680
|
-
);
|
|
4681
|
-
}
|
|
4682
|
-
function printError(message) {
|
|
4683
|
-
console.error(`
|
|
4684
|
-
${styleText(["bold", "red"], "Error")} ${message}
|
|
4685
|
-
`);
|
|
4686
|
-
}
|
|
4687
|
-
function printHelp() {
|
|
4688
|
-
printHeading("server");
|
|
4689
|
-
console.log(` ${styleText("bold", "Usage")} agentrific <command> [options]
|
|
4690
|
-
`);
|
|
4691
|
-
for (const [command, description] of [
|
|
4692
|
-
["init", "Configure the server"],
|
|
4693
|
-
["start", "Start the server \xB7 press p to pair"],
|
|
4694
|
-
["pair", "Print a fresh pairing QR code and PIN"],
|
|
4695
|
-
["devices", "List enrolled devices"],
|
|
4696
|
-
["revoke <id>", "Revoke a device's access"],
|
|
4697
|
-
["status", "Show server status"],
|
|
4698
|
-
["maintenance enter/resume/drain", "Pause new work, resume, or wait for work to finish"],
|
|
4699
|
-
["service install/uninstall/restart", "Manage the background service"],
|
|
4700
|
-
["logs", "Follow server logs"],
|
|
4701
|
-
["update --check", "Check for a newer release"]
|
|
4702
|
-
])
|
|
4703
|
-
console.log(` ${styleText("cyan", command.padEnd(34))}${description}`);
|
|
4704
|
-
console.log(`
|
|
4705
|
-
${styleText("bold", "Options")}
|
|
4706
|
-
`);
|
|
4707
|
-
for (const [option, description] of [
|
|
4708
|
-
["--data-dir <path>", "Use a specific server data directory"],
|
|
4709
|
-
["--public-url <https-url>", "Use your tunnel URL without prompting"],
|
|
4710
|
-
["--no-interactive", "Disable prompts and keyboard shortcuts"],
|
|
4711
|
-
["--show-payload", "Print manual JSON with the pairing QR code"],
|
|
4712
|
-
["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
|
|
4713
|
-
["--version", "Print the server version"],
|
|
4714
|
-
["--help", "Show this help"]
|
|
4715
|
-
])
|
|
4716
|
-
console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
|
|
4717
|
-
console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
|
|
4718
|
-
}
|
|
4719
|
-
|
|
4720
|
-
// src/pairing.ts
|
|
4721
|
-
function printManualPairing(invitation) {
|
|
4722
|
-
if (Date.now() >= invitation.expiresAt) {
|
|
4723
|
-
printError("This invitation expired. Generate a fresh pairing QR code.");
|
|
4724
|
-
return;
|
|
4725
|
-
}
|
|
4726
|
-
console.log(styleText2("dim", "\n Manual entry \xB7 paste this JSON into Pairing payload:\n"));
|
|
4727
|
-
console.log(` ${invitation.payload}
|
|
4728
|
-
`);
|
|
4729
|
-
}
|
|
4730
|
-
function waitForManualPairing(invitation) {
|
|
4731
|
-
console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
|
|
4732
|
-
return new Promise((resolve) => {
|
|
4733
|
-
const input = process.stdin;
|
|
4734
|
-
const previousRaw = input.isRaw;
|
|
4735
|
-
const timeout = setTimeout(
|
|
4736
|
-
() => {
|
|
4737
|
-
console.log(
|
|
4738
|
-
styleText2("dim", "\n Pairing invitation expired. Run agentrific pair again.\n")
|
|
4739
|
-
);
|
|
4740
|
-
close();
|
|
4741
|
-
},
|
|
4742
|
-
Math.max(0, invitation.expiresAt - Date.now())
|
|
4743
|
-
);
|
|
4744
|
-
function close() {
|
|
4745
|
-
clearTimeout(timeout);
|
|
4746
|
-
input.removeListener("keypress", pressed);
|
|
4747
|
-
input.removeListener("end", close);
|
|
4748
|
-
process.removeListener("SIGINT", interrupted);
|
|
4749
|
-
process.removeListener("SIGTERM", terminated);
|
|
4750
|
-
input.setRawMode(previousRaw);
|
|
4751
|
-
input.pause();
|
|
4752
|
-
resolve();
|
|
4753
|
-
}
|
|
4754
|
-
function interrupted() {
|
|
4755
|
-
process.exitCode = 130;
|
|
4756
|
-
close();
|
|
4757
|
-
}
|
|
4758
|
-
function terminated() {
|
|
4759
|
-
process.exitCode = 143;
|
|
4760
|
-
close();
|
|
4761
|
-
}
|
|
4762
|
-
function pressed(text, key) {
|
|
4763
|
-
if (key.ctrl && key.name === "c") {
|
|
4764
|
-
interrupted();
|
|
4765
|
-
return;
|
|
4766
|
-
}
|
|
4767
|
-
if (key.ctrl || key.meta) return;
|
|
4768
|
-
if (text?.toLowerCase() === "j") {
|
|
4769
|
-
printManualPairing(invitation);
|
|
4770
|
-
close();
|
|
4771
|
-
} else if (text?.toLowerCase() === "q" || key.name === "return" || key.name === "escape") {
|
|
4772
|
-
close();
|
|
4773
|
-
}
|
|
4774
|
-
}
|
|
4775
|
-
emitKeypressEvents(input);
|
|
4776
|
-
input.setRawMode(true);
|
|
4777
|
-
input.on("keypress", pressed);
|
|
4778
|
-
input.once("end", close);
|
|
4779
|
-
process.once("SIGINT", interrupted);
|
|
4780
|
-
process.once("SIGTERM", terminated);
|
|
4781
|
-
input.resume();
|
|
4782
|
-
});
|
|
4783
|
-
}
|
|
4784
|
-
function pairingOrigin(url) {
|
|
4785
|
-
const endpoint = new URL(url);
|
|
4786
|
-
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
4787
|
-
throw new Error("Use the HTTPS server origin without a path or credentials.");
|
|
4788
|
-
if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
|
|
4789
|
-
throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
|
|
4790
|
-
return endpoint.origin;
|
|
4791
|
-
}
|
|
4792
|
-
async function printPairing(dataDir, url) {
|
|
4793
|
-
if (!url)
|
|
4794
|
-
throw new Error(
|
|
4795
|
-
"Specify --public-url with your Tailscale Serve or Cloudflare Tunnel HTTPS address."
|
|
4796
|
-
);
|
|
4797
|
-
const origin = pairingOrigin(url);
|
|
4798
|
-
const grant = z4.object({
|
|
4799
|
-
pairingId: z4.string(),
|
|
4800
|
-
pin: z4.string(),
|
|
4801
|
-
expiresAt: z4.number(),
|
|
4802
|
-
serverId: z4.string()
|
|
4803
|
-
}).parse(await controlRequest(dataDir, { command: "pair" }));
|
|
4804
|
-
const invitation = {
|
|
4805
|
-
v: 1,
|
|
4806
|
-
url: origin,
|
|
4807
|
-
serverId: grant.serverId,
|
|
4808
|
-
pairingId: grant.pairingId
|
|
4809
|
-
};
|
|
4810
|
-
const payload = JSON.stringify(invitation);
|
|
4811
|
-
const link = createPairingLink(invitation);
|
|
4812
|
-
printHeading("pairing");
|
|
4813
|
-
console.log(" 1. Scan below with your phone camera and tap Open Agentrific if offered.");
|
|
4814
|
-
console.log(" 2. Or open Agentrific and choose Scan pairing QR code.");
|
|
4815
|
-
console.log(" 3. Enter the PIN, then confirm pairing.\n");
|
|
4816
|
-
qrcode.generate(link, { small: true }, (code) => {
|
|
4817
|
-
console.log(
|
|
4818
|
-
code.split("\n").map((line) => ` ${line}`).join("\n")
|
|
4819
|
-
);
|
|
4820
|
-
});
|
|
4821
|
-
console.log();
|
|
4822
|
-
printDetail("PIN:", styleText2(["bold", "yellow"], grant.pin));
|
|
4823
|
-
printDetail("Server", styleText2("cyan", origin));
|
|
4824
|
-
printDetail("Expires", "In five minutes \xB7 one device per code");
|
|
4825
|
-
console.log("\n Pairing grants this device access to run agents as your host user.\n");
|
|
4826
|
-
return { payload, expiresAt: grant.expiresAt };
|
|
4827
|
-
}
|
|
4828
|
-
|
|
4829
4889
|
// src/terminal.ts
|
|
4890
|
+
init_pairing();
|
|
4891
|
+
init_cli_output();
|
|
4830
4892
|
import { createInterface } from "node:readline/promises";
|
|
4831
4893
|
import select from "@inquirer/select";
|
|
4832
4894
|
import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
|
|
4833
4895
|
import { styleText as styleText3 } from "node:util";
|
|
4834
4896
|
|
|
4835
4897
|
// src/tunnels.ts
|
|
4898
|
+
init_cli_output();
|
|
4836
4899
|
import { spawn } from "node:child_process";
|
|
4837
4900
|
import { access, stat } from "node:fs/promises";
|
|
4838
4901
|
import { constants } from "node:fs";
|
|
@@ -5301,6 +5364,7 @@ function startTerminal(dataDir, publicUrl, tunnel) {
|
|
|
5301
5364
|
}
|
|
5302
5365
|
|
|
5303
5366
|
// src/cli.ts
|
|
5367
|
+
init_cli_output();
|
|
5304
5368
|
var ConfigSchema = z18.object({
|
|
5305
5369
|
dataDir: z18.string(),
|
|
5306
5370
|
projectsDir: z18.string(),
|