@decentnetwork/lan 0.1.21 → 0.1.23
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/cli/commands.d.ts +31 -0
- package/dist/cli/commands.js +217 -2
- package/dist/cli/index.js +22 -1
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.js +22 -5
- package/dist/dora/dora-integration.d.ts +14 -0
- package/dist/dora/dora-integration.js +36 -0
- package/docs/INSTALL.md +44 -13
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -265,3 +265,34 @@ export declare function cmdDoraAutofriend(args: {
|
|
|
265
265
|
values?: string[];
|
|
266
266
|
configDir?: string;
|
|
267
267
|
}): Promise<void>;
|
|
268
|
+
/**
|
|
269
|
+
* Install the daemon as a persistent system service.
|
|
270
|
+
*
|
|
271
|
+
* Linux → writes /etc/systemd/system/agentnet.service, daemon-reload,
|
|
272
|
+
* enable+start. Sets ExecStart to the resolved `agentnet`
|
|
273
|
+
* binary and threads --config-dir so sudo's $HOME=/root
|
|
274
|
+
* doesn't pick up an empty /root/.agentnet.
|
|
275
|
+
* macOS → writes /Library/LaunchDaemons/com.decentlan.agentnet.plist,
|
|
276
|
+
* launchctl load (RunAtLoad + KeepAlive).
|
|
277
|
+
*
|
|
278
|
+
* Both surfaces stream stdout/stderr to /var/log/agentnet.log so a
|
|
279
|
+
* single `tail -f /var/log/agentnet.log` works regardless of platform.
|
|
280
|
+
* `--uninstall` reverses each respectively.
|
|
281
|
+
*
|
|
282
|
+
* Requires root (writes to /etc or /Library). On Linux the resolved
|
|
283
|
+
* unit will run as User=root; on macOS the LaunchDaemon already runs
|
|
284
|
+
* as root. The CLI complains loudly with the missing-sudo hint if
|
|
285
|
+
* the writes fail with EACCES.
|
|
286
|
+
*/
|
|
287
|
+
export declare function cmdServiceInstall(args: {
|
|
288
|
+
uninstall?: boolean;
|
|
289
|
+
configDir?: string;
|
|
290
|
+
}): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Show whether the service is installed + running. Cross-platform
|
|
293
|
+
* pass-through to systemctl status / launchctl list with a one-line
|
|
294
|
+
* up/down summary up top.
|
|
295
|
+
*/
|
|
296
|
+
export declare function cmdServiceStatus(_args: {
|
|
297
|
+
configDir?: string;
|
|
298
|
+
}): Promise<void>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { resolve, dirname } from "path";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
6
6
|
import { createConnection } from "net";
|
|
7
|
-
import { ConfigLoader } from "../config/loader.js";
|
|
7
|
+
import { ConfigLoader, DEFAULT_DORA_USERID, DEFAULT_DORA_ADDRESS } from "../config/loader.js";
|
|
8
8
|
import { ipcSocketPath } from "../daemon/ipc.js";
|
|
9
9
|
import { DaemonServer } from "../daemon/server.js";
|
|
10
10
|
import { Ipam } from "../ipam/ipam.js";
|
|
@@ -129,7 +129,31 @@ export async function cmdInit(args) {
|
|
|
129
129
|
console.log(` Config dir: ${dir}`);
|
|
130
130
|
console.log(` Subnet: ${config.network.subnet}`);
|
|
131
131
|
console.log(` Interface: ${config.network.interface}`);
|
|
132
|
-
|
|
132
|
+
// Send the one-time friend-request to the default dora so the daemon
|
|
133
|
+
// can register on first start. Best-effort: a friend-request requires
|
|
134
|
+
// joinNetwork + an announce round, ~10-30s; we cap the wait and
|
|
135
|
+
// surface a hint if it doesn't go through. Skipped when the user
|
|
136
|
+
// overrode dora.userids away from the default (i.e. they're pointing
|
|
137
|
+
// at a private dora — they'll run `agentnet dora enable` themselves).
|
|
138
|
+
const defaultDoraConfigured = (config.dora?.userids ?? []).includes(DEFAULT_DORA_USERID);
|
|
139
|
+
if (defaultDoraConfigured) {
|
|
140
|
+
console.log(`\nFriending the public dora (${DEFAULT_DORA_USERID.slice(0, 16)}...) so the daemon can join the shared network on first start.`);
|
|
141
|
+
try {
|
|
142
|
+
await cmdFriendRequest({
|
|
143
|
+
address: DEFAULT_DORA_ADDRESS,
|
|
144
|
+
hello: `decentlan init (${nodeName})`,
|
|
145
|
+
waitMs: 8000,
|
|
146
|
+
configDir: dir,
|
|
147
|
+
});
|
|
148
|
+
console.log(` Friend-request dispatched. Auto-accept on the dora side will take it from here.`);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
console.warn(` Friend-request to the default dora failed: ${msg}`);
|
|
153
|
+
console.warn(` Re-run later: agentnet dora enable --address ${DEFAULT_DORA_ADDRESS}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log(`\nNext: sudo agentnet service install # or 'agentnet up --real-tun' to run in foreground`);
|
|
133
157
|
}
|
|
134
158
|
/**
|
|
135
159
|
* Show identity information.
|
|
@@ -1238,3 +1262,194 @@ export async function cmdDoraAutofriend(args) {
|
|
|
1238
1262
|
}
|
|
1239
1263
|
console.log(`Takes effect on the next roster refresh (~60s) or daemon restart.`);
|
|
1240
1264
|
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Install the daemon as a persistent system service.
|
|
1267
|
+
*
|
|
1268
|
+
* Linux → writes /etc/systemd/system/agentnet.service, daemon-reload,
|
|
1269
|
+
* enable+start. Sets ExecStart to the resolved `agentnet`
|
|
1270
|
+
* binary and threads --config-dir so sudo's $HOME=/root
|
|
1271
|
+
* doesn't pick up an empty /root/.agentnet.
|
|
1272
|
+
* macOS → writes /Library/LaunchDaemons/com.decentlan.agentnet.plist,
|
|
1273
|
+
* launchctl load (RunAtLoad + KeepAlive).
|
|
1274
|
+
*
|
|
1275
|
+
* Both surfaces stream stdout/stderr to /var/log/agentnet.log so a
|
|
1276
|
+
* single `tail -f /var/log/agentnet.log` works regardless of platform.
|
|
1277
|
+
* `--uninstall` reverses each respectively.
|
|
1278
|
+
*
|
|
1279
|
+
* Requires root (writes to /etc or /Library). On Linux the resolved
|
|
1280
|
+
* unit will run as User=root; on macOS the LaunchDaemon already runs
|
|
1281
|
+
* as root. The CLI complains loudly with the missing-sudo hint if
|
|
1282
|
+
* the writes fail with EACCES.
|
|
1283
|
+
*/
|
|
1284
|
+
export async function cmdServiceInstall(args) {
|
|
1285
|
+
// Detect sudo's $HOME=/root trap. When `service install` is run via
|
|
1286
|
+
// sudo and no --config-dir is provided, derive the home dir from
|
|
1287
|
+
// SUDO_USER instead of os.homedir() — otherwise the unit's
|
|
1288
|
+
// ExecStart points at /root/.agentnet which is empty and the daemon
|
|
1289
|
+
// immediately crash-loops. Falls back to defaultConfigDir() when
|
|
1290
|
+
// SUDO_USER isn't set (non-sudo invocation).
|
|
1291
|
+
let dir;
|
|
1292
|
+
if (args.configDir) {
|
|
1293
|
+
dir = args.configDir;
|
|
1294
|
+
}
|
|
1295
|
+
else if (process.env.SUDO_USER && process.env.SUDO_USER !== "root") {
|
|
1296
|
+
const { execSync } = await import("child_process");
|
|
1297
|
+
try {
|
|
1298
|
+
const sudoHome = execSync(`getent passwd ${process.env.SUDO_USER} | cut -d: -f6`, { encoding: "utf-8" }).trim();
|
|
1299
|
+
dir = sudoHome
|
|
1300
|
+
? `${sudoHome}/.agentnet`
|
|
1301
|
+
: ConfigLoader.defaultConfigDir();
|
|
1302
|
+
console.log(`[service install] sudo detected — using ${dir} (override with --config-dir)`);
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
dir = ConfigLoader.defaultConfigDir();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
dir = ConfigLoader.defaultConfigDir();
|
|
1310
|
+
}
|
|
1311
|
+
const { spawnSync, execSync } = await import("child_process");
|
|
1312
|
+
if (process.platform === "linux") {
|
|
1313
|
+
const unitPath = "/etc/systemd/system/agentnet.service";
|
|
1314
|
+
if (args.uninstall) {
|
|
1315
|
+
spawnSync("systemctl", ["disable", "--now", "agentnet"], { stdio: "inherit" });
|
|
1316
|
+
try {
|
|
1317
|
+
const { unlinkSync, existsSync } = await import("fs");
|
|
1318
|
+
if (existsSync(unitPath))
|
|
1319
|
+
unlinkSync(unitPath);
|
|
1320
|
+
execSync("systemctl daemon-reload");
|
|
1321
|
+
console.log(`Removed ${unitPath} and disabled the service.`);
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1325
|
+
throw new Error(`Could not remove ${unitPath} (need root?): ${msg}`);
|
|
1326
|
+
}
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
// Resolve the `agentnet` binary. argv[1] is dist/cli/index.js when
|
|
1330
|
+
// invoked as `node …/index.js`, so a `which` lookup is more
|
|
1331
|
+
// portable. Fall back to /usr/local/bin/agentnet (npm-default
|
|
1332
|
+
// global prefix on most Linux setups) if the lookup fails.
|
|
1333
|
+
let agentnetBin;
|
|
1334
|
+
try {
|
|
1335
|
+
agentnetBin = execSync("command -v agentnet", { encoding: "utf-8" }).trim();
|
|
1336
|
+
if (!agentnetBin)
|
|
1337
|
+
throw new Error("not found");
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
agentnetBin = "/usr/local/bin/agentnet";
|
|
1341
|
+
}
|
|
1342
|
+
const unit = `[Unit]
|
|
1343
|
+
Description=Decent AgentNet daemon
|
|
1344
|
+
After=network-online.target
|
|
1345
|
+
Wants=network-online.target
|
|
1346
|
+
|
|
1347
|
+
[Service]
|
|
1348
|
+
Type=simple
|
|
1349
|
+
User=root
|
|
1350
|
+
ExecStart=${agentnetBin} up --real-tun --config-dir ${dir}
|
|
1351
|
+
Restart=on-failure
|
|
1352
|
+
RestartSec=5
|
|
1353
|
+
StandardOutput=append:/var/log/agentnet.log
|
|
1354
|
+
StandardError=append:/var/log/agentnet.log
|
|
1355
|
+
|
|
1356
|
+
[Install]
|
|
1357
|
+
WantedBy=multi-user.target
|
|
1358
|
+
`;
|
|
1359
|
+
try {
|
|
1360
|
+
const fs = await import("fs/promises");
|
|
1361
|
+
await fs.writeFile(unitPath, unit, "utf-8");
|
|
1362
|
+
}
|
|
1363
|
+
catch (err) {
|
|
1364
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1365
|
+
throw new Error(`Could not write ${unitPath} (need root? try 'sudo'): ${msg}`);
|
|
1366
|
+
}
|
|
1367
|
+
execSync("systemctl daemon-reload");
|
|
1368
|
+
execSync("systemctl enable --now agentnet");
|
|
1369
|
+
console.log(`Installed ${unitPath} and started agentnet.service.`);
|
|
1370
|
+
console.log(`Logs: journalctl -u agentnet -f (or /var/log/agentnet.log)`);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (process.platform === "darwin") {
|
|
1374
|
+
const plistPath = "/Library/LaunchDaemons/com.decentlan.agentnet.plist";
|
|
1375
|
+
if (args.uninstall) {
|
|
1376
|
+
spawnSync("launchctl", ["unload", plistPath], { stdio: "inherit" });
|
|
1377
|
+
try {
|
|
1378
|
+
const { unlinkSync, existsSync } = await import("fs");
|
|
1379
|
+
if (existsSync(plistPath))
|
|
1380
|
+
unlinkSync(plistPath);
|
|
1381
|
+
console.log(`Unloaded and removed ${plistPath}.`);
|
|
1382
|
+
}
|
|
1383
|
+
catch (err) {
|
|
1384
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1385
|
+
throw new Error(`Could not remove ${plistPath} (need root?): ${msg}`);
|
|
1386
|
+
}
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
let agentnetBin;
|
|
1390
|
+
try {
|
|
1391
|
+
agentnetBin = execSync("command -v agentnet", { encoding: "utf-8" }).trim();
|
|
1392
|
+
if (!agentnetBin)
|
|
1393
|
+
throw new Error("not found");
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
agentnetBin = "/usr/local/bin/agentnet";
|
|
1397
|
+
}
|
|
1398
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1399
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1400
|
+
<plist version="1.0"><dict>
|
|
1401
|
+
<key>Label</key><string>com.decentlan.agentnet</string>
|
|
1402
|
+
<key>UserName</key><string>root</string>
|
|
1403
|
+
<key>ProgramArguments</key><array>
|
|
1404
|
+
<string>${agentnetBin}</string>
|
|
1405
|
+
<string>up</string>
|
|
1406
|
+
<string>--real-tun</string>
|
|
1407
|
+
<string>--config-dir</string>
|
|
1408
|
+
<string>${dir}</string>
|
|
1409
|
+
</array>
|
|
1410
|
+
<key>RunAtLoad</key><true/>
|
|
1411
|
+
<key>KeepAlive</key><true/>
|
|
1412
|
+
<key>StandardOutPath</key><string>/var/log/agentnet.log</string>
|
|
1413
|
+
<key>StandardErrorPath</key><string>/var/log/agentnet.log</string>
|
|
1414
|
+
</dict></plist>
|
|
1415
|
+
`;
|
|
1416
|
+
try {
|
|
1417
|
+
const fs = await import("fs/promises");
|
|
1418
|
+
await fs.writeFile(plistPath, plist, "utf-8");
|
|
1419
|
+
}
|
|
1420
|
+
catch (err) {
|
|
1421
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1422
|
+
throw new Error(`Could not write ${plistPath} (need root? try 'sudo'): ${msg}`);
|
|
1423
|
+
}
|
|
1424
|
+
execSync(`launchctl load ${plistPath}`);
|
|
1425
|
+
console.log(`Installed ${plistPath} and started com.decentlan.agentnet.`);
|
|
1426
|
+
console.log(`Logs: tail -f /var/log/agentnet.log`);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
throw new Error(`'service install' isn't wired up for ${process.platform}. Run 'agentnet up --real-tun' in the foreground for now.`);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Show whether the service is installed + running. Cross-platform
|
|
1433
|
+
* pass-through to systemctl status / launchctl list with a one-line
|
|
1434
|
+
* up/down summary up top.
|
|
1435
|
+
*/
|
|
1436
|
+
export async function cmdServiceStatus(_args) {
|
|
1437
|
+
const { spawnSync } = await import("child_process");
|
|
1438
|
+
if (process.platform === "linux") {
|
|
1439
|
+
const r = spawnSync("systemctl", ["status", "agentnet", "--no-pager"], { encoding: "utf-8" });
|
|
1440
|
+
process.stdout.write(r.stdout || "");
|
|
1441
|
+
if (r.stderr)
|
|
1442
|
+
process.stderr.write(r.stderr);
|
|
1443
|
+
process.exit(r.status ?? 0);
|
|
1444
|
+
}
|
|
1445
|
+
if (process.platform === "darwin") {
|
|
1446
|
+
const r = spawnSync("launchctl", ["list", "com.decentlan.agentnet"], { encoding: "utf-8" });
|
|
1447
|
+
if ((r.status ?? 0) !== 0) {
|
|
1448
|
+
console.log("Not loaded. Install with 'sudo agentnet service install'.");
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
process.stdout.write(r.stdout || "");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
console.log(`'service status' isn't wired up for ${process.platform}.`);
|
|
1455
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
10
10
|
// Belt-and-braces — also raise it here in case the CLI is run directly
|
|
11
11
|
// (e.g. `node dist/cli/index.js` rather than via dist/index.js).
|
|
12
12
|
EventEmitter.defaultMaxListeners = 100;
|
|
13
|
-
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDnsInstall, cmdDnsHosts, } from "./commands.js";
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdServiceStatus, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -97,6 +97,27 @@ async function main() {
|
|
|
97
97
|
})
|
|
98
98
|
.demandCommand(1, "Specify a dns subcommand (run 'agentnet dns --help')"), () => {
|
|
99
99
|
// parent handler — never invoked because demandCommand
|
|
100
|
+
})
|
|
101
|
+
// Persistent system service — wraps systemctl (Linux) and
|
|
102
|
+
// launchctl (macOS) so a new operator runs ONE command to install
|
|
103
|
+
// + start + persist instead of hand-writing a unit/plist.
|
|
104
|
+
.command("service", "Install/uninstall/inspect the daemon as a system service (systemd on Linux, launchd on macOS)", (y) => y
|
|
105
|
+
.command("install", "Write the unit/plist, enable + start (needs sudo)", (yy) => yy
|
|
106
|
+
.option("uninstall", { type: "boolean", default: false, describe: "Reverse the install" })
|
|
107
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
108
|
+
await cmdServiceInstall({
|
|
109
|
+
uninstall: argv.uninstall,
|
|
110
|
+
configDir: argv["config-dir"],
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
.command("uninstall", "Stop and remove the system service (alias for 'service install --uninstall')", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
114
|
+
await cmdServiceInstall({ uninstall: true, configDir: argv["config-dir"] });
|
|
115
|
+
})
|
|
116
|
+
.command("status", "Show service status (systemctl status / launchctl list)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
117
|
+
await cmdServiceStatus({ configDir: argv["config-dir"] });
|
|
118
|
+
})
|
|
119
|
+
.demandCommand(1, "Specify a service subcommand (run 'agentnet service --help')"), () => {
|
|
120
|
+
// parent handler — never invoked because demandCommand above
|
|
100
121
|
})
|
|
101
122
|
.command("up", "Start the daemon", (y) => y
|
|
102
123
|
.option("name", { type: "string" })
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { DecentAgentNetConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Public dora server baked into `agentnet init` so an operator can join
|
|
4
|
+
* the canonical Decent AgentNet without first hunting down a registry
|
|
5
|
+
* address. Override by editing `dora.userids` (and re-running
|
|
6
|
+
* `agentnet dora enable --address <yours>`) — or by running a private
|
|
7
|
+
* dora and pointing your own peers at it.
|
|
8
|
+
*
|
|
9
|
+
* Both fields are required: the daemon needs `userid` to talk to the
|
|
10
|
+
* server over Carrier, and `address` so `agentnet init` can send the
|
|
11
|
+
* one-time friend-request that establishes the Carrier session.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_DORA_USERID = "98rsHv17h8G6AP9RagyrBiT1kmw4cn8MFPEembS6ZVjv";
|
|
14
|
+
export declare const DEFAULT_DORA_ADDRESS = "Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd";
|
|
2
15
|
export declare class ConfigLoader {
|
|
3
16
|
static defaultConfigPath(): string;
|
|
4
17
|
static defaultConfigDir(): string;
|
package/dist/config/loader.js
CHANGED
|
@@ -23,6 +23,19 @@ const DEFAULT_BOOTSTRAP_NODES = [
|
|
|
23
23
|
const DEFAULT_EXPRESS_NODES = [
|
|
24
24
|
{ host: "lens.beagle.chat", port: 443, pk: "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" },
|
|
25
25
|
];
|
|
26
|
+
/**
|
|
27
|
+
* Public dora server baked into `agentnet init` so an operator can join
|
|
28
|
+
* the canonical Decent AgentNet without first hunting down a registry
|
|
29
|
+
* address. Override by editing `dora.userids` (and re-running
|
|
30
|
+
* `agentnet dora enable --address <yours>`) — or by running a private
|
|
31
|
+
* dora and pointing your own peers at it.
|
|
32
|
+
*
|
|
33
|
+
* Both fields are required: the daemon needs `userid` to talk to the
|
|
34
|
+
* server over Carrier, and `address` so `agentnet init` can send the
|
|
35
|
+
* one-time friend-request that establishes the Carrier session.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_DORA_USERID = "98rsHv17h8G6AP9RagyrBiT1kmw4cn8MFPEembS6ZVjv";
|
|
38
|
+
export const DEFAULT_DORA_ADDRESS = "Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd";
|
|
26
39
|
export class ConfigLoader {
|
|
27
40
|
static defaultConfigPath() {
|
|
28
41
|
return DEFAULT_CONFIG_FILE;
|
|
@@ -101,12 +114,16 @@ export class ConfigLoader {
|
|
|
101
114
|
friends: {
|
|
102
115
|
autoAccept: true,
|
|
103
116
|
},
|
|
104
|
-
// Dora integration is
|
|
105
|
-
//
|
|
106
|
-
//
|
|
117
|
+
// Dora integration is ON by default and points at the public
|
|
118
|
+
// canonical dora — `agentnet init` follows up with a one-time
|
|
119
|
+
// friend-request to its address, so a fresh install joins the
|
|
120
|
+
// shared network with zero additional commands. To run in
|
|
121
|
+
// private (no dora) mode: `agentnet dora disable`. To point at
|
|
122
|
+
// your own dora: `agentnet dora enable --address <addr>` (it
|
|
123
|
+
// replaces the default).
|
|
107
124
|
dora: {
|
|
108
|
-
enabled:
|
|
109
|
-
userids: [],
|
|
125
|
+
enabled: true,
|
|
126
|
+
userids: [DEFAULT_DORA_USERID],
|
|
110
127
|
refreshIntervalMs: 60_000,
|
|
111
128
|
// Default: auto-friend every peer in the dora roster. Dora
|
|
112
129
|
// membership IS the trust statement — joining a dora means
|
|
@@ -61,6 +61,20 @@ export declare class DoraIntegration {
|
|
|
61
61
|
* us, and auto-IP allocation is lost. bootstrap() re-derives its
|
|
62
62
|
* own myUserid so we don't need to thread it through.
|
|
63
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* Re-send the friend-request to each configured dora userid that's
|
|
66
|
+
* still in "requested" state. The public express relay can drop
|
|
67
|
+
* friend-requests transiently (HTTP 500s observed in the wild) AND
|
|
68
|
+
* DHT onion discovery can miss freshly-restarted dora announces;
|
|
69
|
+
* either path failing strands a daemon with `status: requested`
|
|
70
|
+
* forever. The Carrier SDK doesn't retry the friend-request on
|
|
71
|
+
* its own, so we kick it again here whenever bootstrap retry runs.
|
|
72
|
+
*
|
|
73
|
+
* We read the address back from peer.friends() — `sendFriendRequest`
|
|
74
|
+
* stored it in the friend record when the original (failed) request
|
|
75
|
+
* went out, so we don't need it threaded through config.
|
|
76
|
+
*/
|
|
77
|
+
private resendStuckFriendRequests;
|
|
64
78
|
private scheduleBootstrapRetry;
|
|
65
79
|
/**
|
|
66
80
|
* Wrap a single register call with up to 3 retries spaced 2s apart.
|
|
@@ -185,12 +185,48 @@ export class DoraIntegration {
|
|
|
185
185
|
* us, and auto-IP allocation is lost. bootstrap() re-derives its
|
|
186
186
|
* own myUserid so we don't need to thread it through.
|
|
187
187
|
*/
|
|
188
|
+
/**
|
|
189
|
+
* Re-send the friend-request to each configured dora userid that's
|
|
190
|
+
* still in "requested" state. The public express relay can drop
|
|
191
|
+
* friend-requests transiently (HTTP 500s observed in the wild) AND
|
|
192
|
+
* DHT onion discovery can miss freshly-restarted dora announces;
|
|
193
|
+
* either path failing strands a daemon with `status: requested`
|
|
194
|
+
* forever. The Carrier SDK doesn't retry the friend-request on
|
|
195
|
+
* its own, so we kick it again here whenever bootstrap retry runs.
|
|
196
|
+
*
|
|
197
|
+
* We read the address back from peer.friends() — `sendFriendRequest`
|
|
198
|
+
* stored it in the friend record when the original (failed) request
|
|
199
|
+
* went out, so we don't need it threaded through config.
|
|
200
|
+
*/
|
|
201
|
+
resendStuckFriendRequests() {
|
|
202
|
+
const userids = this.opts.config.userids ?? [];
|
|
203
|
+
const peer = this.opts.peerManager;
|
|
204
|
+
const friends = peer.getFriends();
|
|
205
|
+
for (const id of userids) {
|
|
206
|
+
const f = friends.find((x) => x.userid === id || x.pubkey === id);
|
|
207
|
+
if (!f)
|
|
208
|
+
continue;
|
|
209
|
+
if (f.status !== "requested")
|
|
210
|
+
continue;
|
|
211
|
+
if (!f.address)
|
|
212
|
+
continue;
|
|
213
|
+
this.logger.info(`Re-sending friend-request to dora ${id.slice(0, 16)}... (status was 'requested')`);
|
|
214
|
+
peer
|
|
215
|
+
.sendFriendRequest(f.address, "decentlan: dora retry")
|
|
216
|
+
.catch((err) => {
|
|
217
|
+
this.logger.debug(`friend-request retry failed for ${id.slice(0, 16)}: ${err instanceof Error ? err.message : err}`);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
188
221
|
scheduleBootstrapRetry() {
|
|
189
222
|
if (this.retryBootstrapTimer || this.registered)
|
|
190
223
|
return;
|
|
191
224
|
const tick = () => {
|
|
192
225
|
if (this.registered)
|
|
193
226
|
return;
|
|
227
|
+
// Re-send the friend-request first — if it was lost to express
|
|
228
|
+
// 500s or DHT misses, this is what unblocks the retry.
|
|
229
|
+
this.resendStuckFriendRequests();
|
|
194
230
|
this.logger.debug("Retrying dora bootstrap…");
|
|
195
231
|
// Don't await — let it run async and clear the timer if it
|
|
196
232
|
// succeeds, otherwise leave the timer running.
|
package/docs/INSTALL.md
CHANGED
|
@@ -35,25 +35,56 @@ specific TUN helper binaries (we ship `linux-amd64`, `linux-arm64`,
|
|
|
35
35
|
agentnet --help
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
## First-time setup
|
|
38
|
+
## First-time setup — three commands
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
|
|
42
|
-
agentnet init --name my-
|
|
41
|
+
sudo npm install -g @decentnetwork/lan
|
|
42
|
+
agentnet init --name my-machine
|
|
43
|
+
sudo agentnet service install --config-dir $HOME/.agentnet
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That's it. `agentnet init` already points at the public dora and sends
|
|
47
|
+
the one-time friend-request; `service install` writes a systemd unit
|
|
48
|
+
(Linux) or LaunchDaemon (macOS) that runs the daemon as root with
|
|
49
|
+
auto-restart and logs to `/var/log/agentnet.log`.
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
# runs dora publishes it). This one command friends dora,
|
|
46
|
-
# derives its userid, and writes the config — no separate
|
|
47
|
-
# friend-request step needed.
|
|
48
|
-
agentnet dora enable --address <dora's Carrier address>
|
|
51
|
+
Wait ~60–90s, then verify:
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
```bash
|
|
54
|
+
agentnet ipam list # should show 6+ peers
|
|
55
|
+
agentnet diag | grep tun # your allocated 10.86.1.x
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> ### If `ipam list` shows only yourself after 2 minutes
|
|
59
|
+
> The friend-request to dora didn't land — usually because the public
|
|
60
|
+
> express relay is having a bad day and Vultr/cloud DHT discovery
|
|
61
|
+
> sometimes misses freshly-restarted dora announces. Two quick
|
|
62
|
+
> recovery steps:
|
|
63
|
+
>
|
|
64
|
+
> 1. Re-send the friend-request directly through your live daemon:
|
|
65
|
+
> ```bash
|
|
66
|
+
> agentnet friend-request Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd
|
|
67
|
+
> ```
|
|
68
|
+
> Then wait 60s and re-check `agentnet ipam list`.
|
|
69
|
+
>
|
|
70
|
+
> 2. If that still doesn't work, the **dora operator** can pre-friend
|
|
71
|
+
> you so you don't depend on the public delivery path: give them
|
|
72
|
+
> your userid (the line `agentnet identity show` prints) and ask
|
|
73
|
+
> them to run a one-shot add on their dora data-dir, then they
|
|
74
|
+
> restart dora. Your next refresh lands instantly.
|
|
75
|
+
|
|
76
|
+
## Pointing at a private dora (skip if you're joining the public network)
|
|
77
|
+
|
|
78
|
+
The public dora is baked into `agentnet init` defaults. To override:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
agentnet dora enable --address <YOUR private dora's full Carrier address>
|
|
82
|
+
sudo systemctl restart agentnet # or 'launchctl unload + load' on macOS
|
|
52
83
|
```
|
|
53
84
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
`agentnet dora enable` replaces the default userid, sends a fresh
|
|
86
|
+
friend-request, and updates the config — one command, no "userid vs
|
|
87
|
+
address" gymnastics.
|
|
57
88
|
|
|
58
89
|
The daemon will:
|
|
59
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|