@decentnetwork/lan 0.1.21 → 0.1.22

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.
Binary file
Binary file
Binary file
Binary file
@@ -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>;
@@ -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
- console.log(`\nNext: agentnet up --name ${nodeName}`);
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,169 @@ 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
+ const dir = args.configDir || ConfigLoader.defaultConfigDir();
1286
+ const { spawnSync, execSync } = await import("child_process");
1287
+ if (process.platform === "linux") {
1288
+ const unitPath = "/etc/systemd/system/agentnet.service";
1289
+ if (args.uninstall) {
1290
+ spawnSync("systemctl", ["disable", "--now", "agentnet"], { stdio: "inherit" });
1291
+ try {
1292
+ const { unlinkSync, existsSync } = await import("fs");
1293
+ if (existsSync(unitPath))
1294
+ unlinkSync(unitPath);
1295
+ execSync("systemctl daemon-reload");
1296
+ console.log(`Removed ${unitPath} and disabled the service.`);
1297
+ }
1298
+ catch (err) {
1299
+ const msg = err instanceof Error ? err.message : String(err);
1300
+ throw new Error(`Could not remove ${unitPath} (need root?): ${msg}`);
1301
+ }
1302
+ return;
1303
+ }
1304
+ // Resolve the `agentnet` binary. argv[1] is dist/cli/index.js when
1305
+ // invoked as `node …/index.js`, so a `which` lookup is more
1306
+ // portable. Fall back to /usr/local/bin/agentnet (npm-default
1307
+ // global prefix on most Linux setups) if the lookup fails.
1308
+ let agentnetBin;
1309
+ try {
1310
+ agentnetBin = execSync("command -v agentnet", { encoding: "utf-8" }).trim();
1311
+ if (!agentnetBin)
1312
+ throw new Error("not found");
1313
+ }
1314
+ catch {
1315
+ agentnetBin = "/usr/local/bin/agentnet";
1316
+ }
1317
+ const unit = `[Unit]
1318
+ Description=Decent AgentNet daemon
1319
+ After=network-online.target
1320
+ Wants=network-online.target
1321
+
1322
+ [Service]
1323
+ Type=simple
1324
+ User=root
1325
+ ExecStart=${agentnetBin} up --real-tun --config-dir ${dir}
1326
+ Restart=on-failure
1327
+ RestartSec=5
1328
+ StandardOutput=append:/var/log/agentnet.log
1329
+ StandardError=append:/var/log/agentnet.log
1330
+
1331
+ [Install]
1332
+ WantedBy=multi-user.target
1333
+ `;
1334
+ try {
1335
+ const fs = await import("fs/promises");
1336
+ await fs.writeFile(unitPath, unit, "utf-8");
1337
+ }
1338
+ catch (err) {
1339
+ const msg = err instanceof Error ? err.message : String(err);
1340
+ throw new Error(`Could not write ${unitPath} (need root? try 'sudo'): ${msg}`);
1341
+ }
1342
+ execSync("systemctl daemon-reload");
1343
+ execSync("systemctl enable --now agentnet");
1344
+ console.log(`Installed ${unitPath} and started agentnet.service.`);
1345
+ console.log(`Logs: journalctl -u agentnet -f (or /var/log/agentnet.log)`);
1346
+ return;
1347
+ }
1348
+ if (process.platform === "darwin") {
1349
+ const plistPath = "/Library/LaunchDaemons/com.decentlan.agentnet.plist";
1350
+ if (args.uninstall) {
1351
+ spawnSync("launchctl", ["unload", plistPath], { stdio: "inherit" });
1352
+ try {
1353
+ const { unlinkSync, existsSync } = await import("fs");
1354
+ if (existsSync(plistPath))
1355
+ unlinkSync(plistPath);
1356
+ console.log(`Unloaded and removed ${plistPath}.`);
1357
+ }
1358
+ catch (err) {
1359
+ const msg = err instanceof Error ? err.message : String(err);
1360
+ throw new Error(`Could not remove ${plistPath} (need root?): ${msg}`);
1361
+ }
1362
+ return;
1363
+ }
1364
+ let agentnetBin;
1365
+ try {
1366
+ agentnetBin = execSync("command -v agentnet", { encoding: "utf-8" }).trim();
1367
+ if (!agentnetBin)
1368
+ throw new Error("not found");
1369
+ }
1370
+ catch {
1371
+ agentnetBin = "/usr/local/bin/agentnet";
1372
+ }
1373
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
1374
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1375
+ <plist version="1.0"><dict>
1376
+ <key>Label</key><string>com.decentlan.agentnet</string>
1377
+ <key>UserName</key><string>root</string>
1378
+ <key>ProgramArguments</key><array>
1379
+ <string>${agentnetBin}</string>
1380
+ <string>up</string>
1381
+ <string>--real-tun</string>
1382
+ <string>--config-dir</string>
1383
+ <string>${dir}</string>
1384
+ </array>
1385
+ <key>RunAtLoad</key><true/>
1386
+ <key>KeepAlive</key><true/>
1387
+ <key>StandardOutPath</key><string>/var/log/agentnet.log</string>
1388
+ <key>StandardErrorPath</key><string>/var/log/agentnet.log</string>
1389
+ </dict></plist>
1390
+ `;
1391
+ try {
1392
+ const fs = await import("fs/promises");
1393
+ await fs.writeFile(plistPath, plist, "utf-8");
1394
+ }
1395
+ catch (err) {
1396
+ const msg = err instanceof Error ? err.message : String(err);
1397
+ throw new Error(`Could not write ${plistPath} (need root? try 'sudo'): ${msg}`);
1398
+ }
1399
+ execSync(`launchctl load ${plistPath}`);
1400
+ console.log(`Installed ${plistPath} and started com.decentlan.agentnet.`);
1401
+ console.log(`Logs: tail -f /var/log/agentnet.log`);
1402
+ return;
1403
+ }
1404
+ throw new Error(`'service install' isn't wired up for ${process.platform}. Run 'agentnet up --real-tun' in the foreground for now.`);
1405
+ }
1406
+ /**
1407
+ * Show whether the service is installed + running. Cross-platform
1408
+ * pass-through to systemctl status / launchctl list with a one-line
1409
+ * up/down summary up top.
1410
+ */
1411
+ export async function cmdServiceStatus(_args) {
1412
+ const { spawnSync } = await import("child_process");
1413
+ if (process.platform === "linux") {
1414
+ const r = spawnSync("systemctl", ["status", "agentnet", "--no-pager"], { encoding: "utf-8" });
1415
+ process.stdout.write(r.stdout || "");
1416
+ if (r.stderr)
1417
+ process.stderr.write(r.stderr);
1418
+ process.exit(r.status ?? 0);
1419
+ }
1420
+ if (process.platform === "darwin") {
1421
+ const r = spawnSync("launchctl", ["list", "com.decentlan.agentnet"], { encoding: "utf-8" });
1422
+ if ((r.status ?? 0) !== 0) {
1423
+ console.log("Not loaded. Install with 'sudo agentnet service install'.");
1424
+ return;
1425
+ }
1426
+ process.stdout.write(r.stdout || "");
1427
+ return;
1428
+ }
1429
+ console.log(`'service status' isn't wired up for ${process.platform}.`);
1430
+ }
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" })
@@ -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;
@@ -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 opt-in. Leave userids empty to fall back to
105
- // manual ipam.yaml mode. Once a dora server's userid is added,
106
- // the daemon registers itself on startup and pulls the roster.
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: false,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
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",