@decentnetwork/lan 0.1.197 → 0.1.199

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/README.md CHANGED
@@ -15,7 +15,7 @@ Perfect for:
15
15
  ### Prerequisites
16
16
 
17
17
  - **Node.js 20+**
18
- - **Linux or macOS** (Windows support planned)
18
+ - **Linux, macOS, or native 64-bit Windows** (x64 / ARM64)
19
19
  - **Carrier identity** (generate with `agentnet init`)
20
20
 
21
21
  ### Installation
@@ -224,8 +224,8 @@ agentnet openclaw diagnose --target <name>.agentnet
224
224
 
225
225
  ## Limitations (MVP v0.1)
226
226
 
227
- - Linux/macOS only (Windows in future)
228
- - TCP only (UDP planned in v0.2)
227
+ - Native Windows uses Wintun and must run from an Administrator PowerShell
228
+ - Kernel NAT/masquerade is Linux-only; the cross-platform CONNECT exit proxy does not need it
229
229
  - No remote desktop protocol support (TBD)
230
230
  - No video/media optimization (rely on Carrier relay)
231
231
  - Static IPAM (blockchain registry planned for v1.0)
@@ -0,0 +1,84 @@
1
+ Prebuilt Binaries License
2
+ -------------------------
3
+
4
+ 1. DEFINITIONS. "Software" means the precise contents of the "wintun.dll"
5
+ files that are included in the .zip file that contains this document as
6
+ downloaded from wintun.net/builds.
7
+
8
+ 2. LICENSE GRANT. WireGuard LLC grants to you a non-exclusive and
9
+ non-transferable right to use Software for lawful purposes under certain
10
+ obligations and limited rights as set forth in this agreement.
11
+
12
+ 3. RESTRICTIONS. Software is owned and copyrighted by WireGuard LLC. It is
13
+ licensed, not sold. Title to Software and all associated intellectual
14
+ property rights are retained by WireGuard. You must not:
15
+ a. reverse engineer, decompile, disassemble, extract from, or otherwise
16
+ modify the Software;
17
+ b. modify or create derivative work based upon Software in whole or in
18
+ parts, except insofar as only the API interfaces of the "wintun.h" file
19
+ distributed alongside the Software (the "Permitted API") are used;
20
+ c. remove any proprietary notices, labels, or copyrights from the Software;
21
+ d. resell, redistribute, lease, rent, transfer, sublicense, or otherwise
22
+ transfer rights of the Software without the prior written consent of
23
+ WireGuard LLC, except insofar as the Software is distributed alongside
24
+ other software that uses the Software only via the Permitted API;
25
+ e. use the name of WireGuard LLC, the WireGuard project, the Wintun
26
+ project, or the names of its contributors to endorse or promote products
27
+ derived from the Software without specific prior written consent.
28
+
29
+ 4. LIMITED WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF
30
+ ANY KIND. WIREGUARD LLC HEREBY EXCLUDES AND DISCLAIMS ALL IMPLIED OR
31
+ STATUTORY WARRANTIES, INCLUDING ANY WARRANTIES OF MERCHANTABILITY, FITNESS
32
+ FOR A PARTICULAR PURPOSE, QUALITY, NON-INFRINGEMENT, TITLE, RESULTS,
33
+ EFFORTS, OR QUIET ENJOYMENT. THERE IS NO WARRANTY THAT THE PRODUCT WILL BE
34
+ ERROR-FREE OR WILL FUNCTION WITHOUT INTERRUPTION. YOU ASSUME THE ENTIRE
35
+ RISK FOR THE RESULTS OBTAINED USING THE PRODUCT. TO THE EXTENT THAT
36
+ WIREGUARD LLC MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW,
37
+ THE SCOPE AND DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER
38
+ SUCH LAW. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
39
+ WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR
40
+ A PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE DISCLAIMED, EXCEPT TO THE
41
+ EXTENT THAT THESE DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.
42
+
43
+ 5. LIMITATION OF LIABILITY. To the extent not prohibited by law, in no event
44
+ WireGuard LLC or any third-party-developer will be liable for any lost
45
+ revenue, profit or data or for special, indirect, consequential, incidental
46
+ or punitive damages, however caused regardless of the theory of liability,
47
+ arising out of or related to the use of or inability to use Software, even
48
+ if WireGuard LLC has been advised of the possibility of such damages.
49
+ Solely you are responsible for determining the appropriateness of using
50
+ Software and accept full responsibility for all risks associated with its
51
+ exercise of rights under this agreement, including but not limited to the
52
+ risks and costs of program errors, compliance with applicable laws, damage
53
+ to or loss of data, programs or equipment, and unavailability or
54
+ interruption of operations. The foregoing limitations will apply even if
55
+ the above stated warranty fails of its essential purpose. You acknowledge,
56
+ that it is in the nature of software that software is complex and not
57
+ completely free of errors. In no event shall WireGuard LLC or any
58
+ third-party-developer be liable to you under any theory for any damages
59
+ suffered by you or any user of Software or for any special, incidental,
60
+ indirect, consequential or similar damages (including without limitation
61
+ damages for loss of business profits, business interruption, loss of
62
+ business information or any other pecuniary loss) arising out of the use or
63
+ inability to use Software, even if WireGuard LLC has been advised of the
64
+ possibility of such damages and regardless of the legal or quitable theory
65
+ (contract, tort, or otherwise) upon which the claim is based.
66
+
67
+ 6. TERMINATION. This agreement is affected until terminated. You may
68
+ terminate this agreement at any time. This agreement will terminate
69
+ immediately without notice from WireGuard LLC if you fail to comply with
70
+ the terms and conditions of this agreement. Upon termination, you must
71
+ delete Software and all copies of Software and cease all forms of
72
+ distribution of Software.
73
+
74
+ 7. SEVERABILITY. If any provision of this agreement is held to be
75
+ unenforceable, this agreement will remain in effect with the provision
76
+ omitted, unless omission would frustrate the intent of the parties, in
77
+ which case this agreement will immediately terminate.
78
+
79
+ 8. RESERVATION OF RIGHTS. All rights not expressly granted in this agreement
80
+ are reserved by WireGuard LLC. For example, WireGuard LLC reserves the
81
+ right at any time to cease development of Software, to alter distribution
82
+ details, features, specifications, capabilities, functions, licensing
83
+ terms, release dates, APIs, ABIs, general availability, or other
84
+ characteristics of the Software.
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": "0.14.1",
3
+ "source": "https://www.wintun.net/builds/wintun-0.14.1.zip",
4
+ "sourceSha256": "07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51",
5
+ "files": {
6
+ "bin/windows-amd64/wintun.dll": "e5da8447dc2c320edc0fc52fa01885c103de8c118481f683643cacc3220dafce",
7
+ "bin/windows-arm64/wintun.dll": "f7ba89005544be9d85231a9e0d5f23b2d15b3311667e2dad0debd344918a3f80",
8
+ "bin/WINTUN-LICENSE.txt": "183adac21e7d96c508c8fd34d394b7b6708bc81564ad1bad61ab66143a008cd2"
9
+ }
10
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -461,16 +461,21 @@ export declare function cmdDoraAutofriend(args: {
461
461
  * doesn't pick up an empty /root/.agentnet.
462
462
  * macOS → writes /Library/LaunchDaemons/com.decentlan.agentnet.plist,
463
463
  * launchctl load (RunAtLoad + KeepAlive).
464
+ * Windows → writes a PowerShell launcher in the config directory and
465
+ * registers an elevated SYSTEM startup task with schtasks.exe.
464
466
  *
465
- * Both surfaces stream stdout/stderr to /var/log/agentnet.log so a
466
- * single `tail -f /var/log/agentnet.log` works regardless of platform.
467
- * `--uninstall` reverses each respectively.
467
+ * Unix services stream stdout/stderr to the journal or
468
+ * /var/log/agentnet.log; Windows logs beside config.yaml.
469
+ * `--uninstall` reverses each platform's installation.
468
470
  *
469
- * Requires root (writes to /etc or /Library). On Linux the resolved
470
- * unit will run as User=root; on macOS the LaunchDaemon already runs
471
- * as root. The CLI complains loudly with the missing-sudo hint if
472
- * the writes fail with EACCES.
473
- */
471
+ * Requires root on Unix or an Administrator PowerShell on Windows.
472
+ */
473
+ export declare function windowsServiceLauncherScript(args: {
474
+ nodeBin: string;
475
+ cliEntry: string;
476
+ configDir: string;
477
+ logPath: string;
478
+ }): string;
474
479
  export declare function cmdServiceInstall(args: {
475
480
  uninstall?: boolean;
476
481
  configDir?: string;
@@ -490,7 +495,7 @@ export declare function cmdServiceStatus(_args: {
490
495
  * no sudo — use that for picking up an upgrade; use this when the service
491
496
  * itself is wedged/stopped and you want launchd/systemd to bring it back.
492
497
  */
493
- export declare function cmdServiceRestart(_args: {
498
+ export declare function cmdServiceRestart(args: {
494
499
  configDir?: string;
495
500
  }): Promise<void>;
496
501
  /**
@@ -909,7 +909,7 @@ export async function cmdProxyEnable(args) {
909
909
  };
910
910
  await ConfigLoader.save(config, configPath);
911
911
  console.log(`Proxy enabled on port ${config.proxy.port}.`);
912
- console.log(`Listener will bind to ${config.network.ip}:${config.proxy.port} on next 'agentnet up'.`);
912
+ console.log(`Listener will bind to this node's runtime AgentNet IP on next 'agentnet up'.`);
913
913
  if ((config.proxy.allowHosts ?? []).length === 0) {
914
914
  console.log(`(No host allowlist set — any host will be accepted as a CONNECT target.)`);
915
915
  console.log(`To restrict: agentnet proxy allow-host '*.binance.com'`);
@@ -947,7 +947,7 @@ export async function cmdProxyStatus(args) {
947
947
  return;
948
948
  }
949
949
  console.log("Proxy: enabled");
950
- console.log(` listen: ${config.network.ip}:${proxy.port}`);
950
+ console.log(` listen: <runtime AgentNet IP>:${proxy.port}`);
951
951
  console.log(` protocol: HTTP CONNECT`);
952
952
  const allowHosts = proxy.allowHosts ?? [];
953
953
  if (allowHosts.length === 0) {
@@ -2100,16 +2100,27 @@ export async function cmdDoraAutofriend(args) {
2100
2100
  * doesn't pick up an empty /root/.agentnet.
2101
2101
  * macOS → writes /Library/LaunchDaemons/com.decentlan.agentnet.plist,
2102
2102
  * launchctl load (RunAtLoad + KeepAlive).
2103
+ * Windows → writes a PowerShell launcher in the config directory and
2104
+ * registers an elevated SYSTEM startup task with schtasks.exe.
2103
2105
  *
2104
- * Both surfaces stream stdout/stderr to /var/log/agentnet.log so a
2105
- * single `tail -f /var/log/agentnet.log` works regardless of platform.
2106
- * `--uninstall` reverses each respectively.
2106
+ * Unix services stream stdout/stderr to the journal or
2107
+ * /var/log/agentnet.log; Windows logs beside config.yaml.
2108
+ * `--uninstall` reverses each platform's installation.
2107
2109
  *
2108
- * Requires root (writes to /etc or /Library). On Linux the resolved
2109
- * unit will run as User=root; on macOS the LaunchDaemon already runs
2110
- * as root. The CLI complains loudly with the missing-sudo hint if
2111
- * the writes fail with EACCES.
2110
+ * Requires root on Unix or an Administrator PowerShell on Windows.
2112
2111
  */
2112
+ export function windowsServiceLauncherScript(args) {
2113
+ const psq = (value) => `'${value.replace(/'/g, "''")}'`;
2114
+ return `$ErrorActionPreference = 'Continue'
2115
+ $env:AGENTNET_SERVICE_WRAPPER = '1'
2116
+ while ($true) {
2117
+ & ${psq(args.nodeBin)} ${psq(args.cliEntry)} up --real-tun --config-dir ${psq(args.configDir)} *>> ${psq(args.logPath)}
2118
+ $code = $LASTEXITCODE
2119
+ "$(Get-Date -Format o) agentnet exited with code $code; restarting in 5 seconds" | Out-File -FilePath ${psq(args.logPath)} -Append -Encoding utf8
2120
+ Start-Sleep -Seconds 5
2121
+ }
2122
+ `;
2123
+ }
2113
2124
  export async function cmdServiceInstall(args) {
2114
2125
  // Detect sudo's $HOME=/root trap. When `service install` is run via
2115
2126
  // sudo and no --config-dir is provided, derive the home dir from
@@ -2150,6 +2161,46 @@ export async function cmdServiceInstall(args) {
2150
2161
  const nodeBin = process.execPath;
2151
2162
  const nodeDir = dirname(nodeBin);
2152
2163
  const servicePath = `${nodeDir}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
2164
+ if (process.platform === "win32") {
2165
+ const taskName = "Decent AgentNet";
2166
+ const scriptPath = resolve(dir, "agentnet-service.ps1");
2167
+ const logPath = resolve(dir, "agentnet-service.log");
2168
+ if (args.uninstall) {
2169
+ spawnSync("schtasks.exe", ["/End", "/TN", taskName], { stdio: "ignore" });
2170
+ const removed = spawnSync("schtasks.exe", ["/Delete", "/F", "/TN", taskName], { stdio: "inherit" });
2171
+ try {
2172
+ const { unlinkSync, existsSync } = await import("fs");
2173
+ if (existsSync(scriptPath))
2174
+ unlinkSync(scriptPath);
2175
+ }
2176
+ catch {
2177
+ // Task removal is the important part; script cleanup is best effort.
2178
+ }
2179
+ if ((removed.status ?? 1) !== 0)
2180
+ throw new Error(`Could not remove Windows scheduled task '${taskName}'`);
2181
+ console.log(`Removed Windows startup task '${taskName}'.`);
2182
+ return;
2183
+ }
2184
+ const cliEntry = resolve(dirname(fileURLToPath(import.meta.url)), "index.js");
2185
+ const psq = (value) => `'${value.replace(/'/g, "''")}'`;
2186
+ const script = windowsServiceLauncherScript({ nodeBin, cliEntry, configDir: dir, logPath });
2187
+ const fs = await import("fs/promises");
2188
+ await fs.mkdir(dir, { recursive: true });
2189
+ await fs.writeFile(scriptPath, script, "utf-8");
2190
+ const taskCommand = `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${scriptPath}"`;
2191
+ const created = spawnSync("schtasks.exe", [
2192
+ "/Create", "/F", "/TN", taskName, "/SC", "ONSTART", "/RU", "SYSTEM", "/RL", "HIGHEST", "/TR", taskCommand,
2193
+ ], { stdio: "inherit" });
2194
+ if ((created.status ?? 1) !== 0) {
2195
+ throw new Error("Could not create the AgentNet Windows startup task. Open PowerShell as Administrator and retry.");
2196
+ }
2197
+ const started = spawnSync("schtasks.exe", ["/Run", "/TN", taskName], { stdio: "inherit" });
2198
+ if ((started.status ?? 1) !== 0)
2199
+ throw new Error(`Created '${taskName}' but could not start it`);
2200
+ console.log(`Installed and started Windows startup task '${taskName}'.`);
2201
+ console.log(`Logs: Get-Content -Wait ${psq(logPath)}`);
2202
+ return;
2203
+ }
2153
2204
  if (process.platform === "linux") {
2154
2205
  const unitPath = "/etc/systemd/system/agentnet.service";
2155
2206
  if (args.uninstall) {
@@ -2303,6 +2354,15 @@ export async function cmdServiceStatus(_args) {
2303
2354
  process.stdout.write(r.stdout || "");
2304
2355
  return;
2305
2356
  }
2357
+ if (process.platform === "win32") {
2358
+ const r = spawnSync("schtasks.exe", ["/Query", "/TN", "Decent AgentNet", "/V", "/FO", "LIST"], { encoding: "utf-8" });
2359
+ if ((r.status ?? 0) !== 0) {
2360
+ console.log("Not installed. Open PowerShell as Administrator and run 'agentnet service install'.");
2361
+ return;
2362
+ }
2363
+ process.stdout.write(r.stdout || "");
2364
+ return;
2365
+ }
2306
2366
  console.log(`'service status' isn't wired up for ${process.platform}.`);
2307
2367
  }
2308
2368
  /**
@@ -2312,7 +2372,7 @@ export async function cmdServiceStatus(_args) {
2312
2372
  * no sudo — use that for picking up an upgrade; use this when the service
2313
2373
  * itself is wedged/stopped and you want launchd/systemd to bring it back.
2314
2374
  */
2315
- export async function cmdServiceRestart(_args) {
2375
+ export async function cmdServiceRestart(args) {
2316
2376
  const { spawnSync } = await import("child_process");
2317
2377
  if (process.platform === "linux") {
2318
2378
  const r = spawnSync("systemctl", ["restart", "agentnet"], { stdio: "inherit" });
@@ -2344,6 +2404,34 @@ export async function cmdServiceRestart(_args) {
2344
2404
  console.log(`Restarted ${label}. Logs: tail -f /var/log/agentnet.log`);
2345
2405
  return;
2346
2406
  }
2407
+ if (process.platform === "win32") {
2408
+ // Prefer the daemon's graceful restart path: it closes Carrier sessions,
2409
+ // stdin-signals the Wintun helper, releases IPC/routes, and then exits.
2410
+ // The scheduled-task wrapper notices the exit and relaunches it. A raw
2411
+ // schtasks /End is only the recovery fallback when IPC is unavailable.
2412
+ try {
2413
+ const dir = args.configDir || ConfigLoader.defaultConfigDir();
2414
+ const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
2415
+ if (daemonPid(config) !== null) {
2416
+ const res = await ipcCall(config, { op: "self-restart" });
2417
+ if (res.ok) {
2418
+ console.log(`Graceful restart requested for '${"Decent AgentNet"}'.`);
2419
+ return;
2420
+ }
2421
+ }
2422
+ }
2423
+ catch {
2424
+ // No reachable daemon: fall through and recover the scheduled task.
2425
+ }
2426
+ const taskName = "Decent AgentNet";
2427
+ spawnSync("schtasks.exe", ["/End", "/TN", taskName], { stdio: "ignore" });
2428
+ const run = spawnSync("schtasks.exe", ["/Run", "/TN", taskName], { stdio: "inherit" });
2429
+ if ((run.status ?? 1) !== 0) {
2430
+ throw new Error(`Could not restart '${taskName}'. Open PowerShell as Administrator and run 'agentnet service install'.`);
2431
+ }
2432
+ console.log(`Restarted '${taskName}'.`);
2433
+ return;
2434
+ }
2347
2435
  throw new Error(`'service restart' isn't wired up for ${process.platform}.`);
2348
2436
  }
2349
2437
  /**
package/dist/cli/index.js CHANGED
@@ -116,10 +116,10 @@ async function main() {
116
116
  .demandCommand(1, "Specify a dns subcommand (run 'agentnet dns --help')"), () => {
117
117
  // parent handler — never invoked because demandCommand
118
118
  })
119
- // Persistent system service — wraps systemctl (Linux) and
120
- // launchctl (macOS) so a new operator runs ONE command to install
119
+ // Persistent system service — wraps systemctl (Linux), launchctl
120
+ // (macOS), or a highest-privilege startup task (Windows) so a new operator runs ONE command to install
121
121
  // + start + persist instead of hand-writing a unit/plist.
122
- .command("service", "Install/uninstall/inspect the daemon as a system service (systemd on Linux, launchd on macOS)", (y) => y
122
+ .command("service", "Install/uninstall/inspect the daemon as a system service/startup task", (y) => y
123
123
  .command("install", "Write the unit/plist, enable + start (needs sudo)", (yy) => yy
124
124
  .option("uninstall", { type: "boolean", default: false, describe: "Reverse the install" })
125
125
  .option("config-dir", { type: "string" }), async (argv) => {
@@ -881,6 +881,16 @@ var ConfigLoader = class {
881
881
  }
882
882
  };
883
883
 
884
+ // src/daemon/ipc.ts
885
+ import { createHash } from "crypto";
886
+ function ipcSocketPath(dataDir, platform = process.platform) {
887
+ if (platform === "win32") {
888
+ const id = createHash("sha256").update(dataDir.toLowerCase()).digest("hex").slice(0, 16);
889
+ return `\\\\.\\pipe\\agentnet-${id}`;
890
+ }
891
+ return `${dataDir.replace(/\/+$/, "")}/daemon.sock`;
892
+ }
893
+
884
894
  // src/console/data.ts
885
895
  function fileSize(n) {
886
896
  if (!n || n < 0) return "0 B";
@@ -910,7 +920,7 @@ var DaemonClient = class _DaemonClient {
910
920
  const dir = configDir ?? resolve2(homedir3(), ".agentnet");
911
921
  const config = await ConfigLoader.load(resolve2(dir, "config.yaml"));
912
922
  const dataDir = (config.carrier.dataDir || resolve2(dir, "carrier")).replace(/\/+$/, "");
913
- return new _DaemonClient(`${dataDir}/daemon.sock`);
923
+ return new _DaemonClient(ipcSocketPath(dataDir));
914
924
  }
915
925
  call(req, timeoutMs = 5e3) {
916
926
  return new Promise((res, rej) => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * IPC server — Unix-domain-socket bridge so the CLI can drive the
2
+ * IPC server — local-socket bridge so the CLI can drive the
3
3
  * running daemon's Carrier identity without spawning a second Peer.
4
4
  *
5
5
  * Why: the daemon owns the keypair while it's up. A `node dist/cli
@@ -15,11 +15,11 @@
15
15
  * response, then both sides close. Keeping it one-shot avoids
16
16
  * dealing with multiplexing or reconnect logic for v0.1.
17
17
  *
18
- * Socket location: <dataDir>/daemon.sock same directory as the
19
- * pidfile. Permissions are loosened to 0666 so the user's CLI can
20
- * connect when the daemon ran via sudo (otherwise the socket is
21
- * root-owned). That's acceptable for a single-user box; multi-user
22
- * hardening can come later via SO_PEERCRED filtering.
18
+ * Socket location: <dataDir>/daemon.sock on Unix; a stable named pipe
19
+ * derived from dataDir on Windows. Unix permissions are loosened to 0666
20
+ * so the user's CLI can connect when the daemon ran via sudo (otherwise
21
+ * the socket is root-owned). That's acceptable for a single-user box;
22
+ * multi-user hardening can come later via peer-credential filtering.
23
23
  */
24
24
  export interface IpcHandlers {
25
25
  /** Send a friend-request via the daemon's Carrier peer. Returns
@@ -63,7 +63,7 @@ export interface IpcHandlers {
63
63
  /** Sign `text` (UTF-8) with this node's Carrier identity key and return the
64
64
  * detached signature (hex) plus our userid. Backs "Sign in with Decent":
65
65
  * the UI server's local-only /connect flow calls this on Approve. Reachable
66
- * ONLY over this Unix socket — never exposed on a TCP interface. */
66
+ * ONLY over this local socket/pipe — never exposed on a TCP interface. */
67
67
  sign: (text: string) => Promise<{
68
68
  sig: string;
69
69
  userid: string;
@@ -147,9 +147,10 @@ export declare class IpcServer {
147
147
  constructor(socketPath: string, handlers: IpcHandlers);
148
148
  start(): Promise<void>;
149
149
  stop(): Promise<void>;
150
+ private isWindowsNamedPipe;
150
151
  private handleConnection;
151
152
  private dispatch;
152
153
  }
153
154
  /** Derive the socket path that pairs with a given carrier data dir.
154
155
  * Kept as a one-liner helper so daemon and client agree. */
155
- export declare function ipcSocketPath(dataDir: string): string;
156
+ export declare function ipcSocketPath(dataDir: string, platform?: NodeJS.Platform): string;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * IPC server — Unix-domain-socket bridge so the CLI can drive the
2
+ * IPC server — local-socket bridge so the CLI can drive the
3
3
  * running daemon's Carrier identity without spawning a second Peer.
4
4
  *
5
5
  * Why: the daemon owns the keypair while it's up. A `node dist/cli
@@ -15,14 +15,15 @@
15
15
  * response, then both sides close. Keeping it one-shot avoids
16
16
  * dealing with multiplexing or reconnect logic for v0.1.
17
17
  *
18
- * Socket location: <dataDir>/daemon.sock same directory as the
19
- * pidfile. Permissions are loosened to 0666 so the user's CLI can
20
- * connect when the daemon ran via sudo (otherwise the socket is
21
- * root-owned). That's acceptable for a single-user box; multi-user
22
- * hardening can come later via SO_PEERCRED filtering.
18
+ * Socket location: <dataDir>/daemon.sock on Unix; a stable named pipe
19
+ * derived from dataDir on Windows. Unix permissions are loosened to 0666
20
+ * so the user's CLI can connect when the daemon ran via sudo (otherwise
21
+ * the socket is root-owned). That's acceptable for a single-user box;
22
+ * multi-user hardening can come later via peer-credential filtering.
23
23
  */
24
24
  import { createServer } from "net";
25
25
  import { chmodSync, existsSync, unlinkSync } from "fs";
26
+ import { createHash } from "crypto";
26
27
  import { Logger } from "../utils/logger.js";
27
28
  export class IpcServer {
28
29
  socketPath;
@@ -38,7 +39,7 @@ export class IpcServer {
38
39
  // A stale socket from a previously-crashed daemon will make
39
40
  // listen() throw EADDRINUSE — unlink it first. Pidfile-guard
40
41
  // already ensures no LIVE daemon is running, so deletion is safe.
41
- if (existsSync(this.socketPath)) {
42
+ if (!this.isWindowsNamedPipe() && existsSync(this.socketPath)) {
42
43
  try {
43
44
  unlinkSync(this.socketPath);
44
45
  }
@@ -57,11 +58,13 @@ export class IpcServer {
57
58
  // 0666 so non-root CLI can reach a sudo-launched daemon. SO_PEERCRED
58
59
  // / SO_PEEREID would be a tighter filter, but for v0.1 the trust
59
60
  // boundary is "anyone with shell access to this box".
60
- try {
61
- chmodSync(this.socketPath, 0o666);
62
- }
63
- catch (err) {
64
- this.logger.warn(`chmod ${this.socketPath}: ${err}`);
61
+ if (!this.isWindowsNamedPipe()) {
62
+ try {
63
+ chmodSync(this.socketPath, 0o666);
64
+ }
65
+ catch (err) {
66
+ this.logger.warn(`chmod ${this.socketPath}: ${err}`);
67
+ }
65
68
  }
66
69
  this.logger.info(`Listening on ${this.socketPath}`);
67
70
  }
@@ -72,14 +75,19 @@ export class IpcServer {
72
75
  this.server.close(() => resolve());
73
76
  });
74
77
  this.server = undefined;
75
- try {
76
- if (existsSync(this.socketPath))
77
- unlinkSync(this.socketPath);
78
- }
79
- catch {
80
- // best-effort
78
+ if (!this.isWindowsNamedPipe()) {
79
+ try {
80
+ if (existsSync(this.socketPath))
81
+ unlinkSync(this.socketPath);
82
+ }
83
+ catch {
84
+ // best-effort
85
+ }
81
86
  }
82
87
  }
88
+ isWindowsNamedPipe() {
89
+ return this.socketPath.startsWith("\\\\.\\pipe\\");
90
+ }
83
91
  handleConnection(sock) {
84
92
  let buf = "";
85
93
  let handled = false;
@@ -266,7 +274,13 @@ export class IpcServer {
266
274
  }
267
275
  /** Derive the socket path that pairs with a given carrier data dir.
268
276
  * Kept as a one-liner helper so daemon and client agree. */
269
- export function ipcSocketPath(dataDir) {
277
+ export function ipcSocketPath(dataDir, platform = process.platform) {
278
+ if (platform === "win32") {
279
+ // Named pipes are the Windows equivalent of Unix-domain sockets. Hash the
280
+ // configured data directory so multiple AgentNet profiles do not collide.
281
+ const id = createHash("sha256").update(dataDir.toLowerCase()).digest("hex").slice(0, 16);
282
+ return `\\\\.\\pipe\\agentnet-${id}`;
283
+ }
270
284
  // Node's net.listen on Unix sockets supports paths up to ~104 bytes
271
285
  // on macOS; <dataDir>/daemon.sock fits comfortably for sane homedirs.
272
286
  return `${dataDir.replace(/\/+$/, "")}/daemon.sock`;
@@ -690,17 +690,34 @@ export class DaemonServer {
690
690
  this.logger.error(`Self-restart cleanup failed: ${err instanceof Error ? err.message : err}`);
691
691
  }
692
692
  try {
693
- // sh -c so we can shell-quote argv safely and use sleep.
694
- // Node's detached flag reparents the relauncher to PID 1.
695
- // At this point stop() has released IPC, TUN and the pidfile;
696
- // the extra delay also lets relay writes flush remotely.
697
- const cmd = `sleep 1 && exec ${shellQuote([node, ...argv])}`;
698
- const child = spawn("sh", ["-c", cmd], {
699
- detached: true,
700
- stdio: "ignore",
701
- env: process.env,
702
- });
703
- child.unref();
693
+ if (process.platform === "win32") {
694
+ // A scheduled-task installation has a PowerShell supervisor
695
+ // that relaunches us after exit; spawning here as well would
696
+ // create two daemons sharing one Carrier identity. Foreground
697
+ // Windows runs still need a detached replacement.
698
+ if (process.env.AGENTNET_SERVICE_WRAPPER !== "1") {
699
+ const child = spawn(node, argv, {
700
+ detached: true,
701
+ stdio: "ignore",
702
+ env: process.env,
703
+ windowsHide: true,
704
+ });
705
+ child.unref();
706
+ }
707
+ }
708
+ else {
709
+ // sh -c so we can shell-quote argv safely and use sleep.
710
+ // Node's detached flag reparents the relauncher to PID 1.
711
+ // At this point stop() has released IPC, TUN and the pidfile;
712
+ // the extra delay also lets relay writes flush remotely.
713
+ const cmd = `sleep 1 && exec ${shellQuote([node, ...argv])}`;
714
+ const child = spawn("sh", ["-c", cmd], {
715
+ detached: true,
716
+ stdio: "ignore",
717
+ env: process.env,
718
+ });
719
+ child.unref();
720
+ }
704
721
  }
705
722
  catch (err) {
706
723
  this.logger.error(`Self-restart spawn failed: ${err instanceof Error ? err.message : err}`);
@@ -1151,7 +1168,7 @@ export class DaemonServer {
1151
1168
  else if (this.config.proxy.egress) {
1152
1169
  this.logger.warn(`Proxy egress=${this.config.proxy.egress} requested but no physical NIC address found; using OS default egress`);
1153
1170
  }
1154
- this.connectProxy = new ConnectProxy({
1171
+ const proxyOptions = {
1155
1172
  bindIp: tunIp,
1156
1173
  port: this.config.proxy.port,
1157
1174
  allowHosts: this.config.proxy.allowHosts ?? [],
@@ -1174,8 +1191,35 @@ export class DaemonServer {
1174
1191
  this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
1175
1192
  }
1176
1193
  : undefined,
1177
- });
1178
- await this.connectProxy.start();
1194
+ };
1195
+ // Windows reports New-NetIPAddress success slightly before Winsock
1196
+ // accepts bind() on that address. A one-shot start therefore races
1197
+ // with Wintun configuration and fails EADDRNOTAVAIL even though the
1198
+ // address becomes usable moments later. Retry only that transient
1199
+ // error; EADDRINUSE and policy/config errors still fail fast.
1200
+ const maxAttempts = process.platform === "win32" ? 40 : 1;
1201
+ let bindError;
1202
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1203
+ const candidate = new ConnectProxy(proxyOptions);
1204
+ try {
1205
+ await candidate.start();
1206
+ this.connectProxy = candidate;
1207
+ bindError = undefined;
1208
+ break;
1209
+ }
1210
+ catch (err) {
1211
+ bindError = err;
1212
+ const code = err.code;
1213
+ if (code !== "EADDRNOTAVAIL" || attempt === maxAttempts)
1214
+ break;
1215
+ if (attempt === 1 || attempt % 10 === 0) {
1216
+ this.logger.warn(`Proxy address ${tunIp} not bindable yet; retrying (${attempt}/${maxAttempts})`);
1217
+ }
1218
+ await new Promise((resolveRetry) => setTimeout(resolveRetry, 250));
1219
+ }
1220
+ }
1221
+ if (!this.connectProxy)
1222
+ throw bindError;
1179
1223
  this.logger.info(`Proxy listening on ${tunIp}:${this.config.proxy.port}`);
1180
1224
  }
1181
1225
  catch (err) {