@agentsoc/beacon 0.0.3 → 0.0.5

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
@@ -98,7 +98,7 @@ Install the background service daemon (systemd on Linux, launchd on macOS):
98
98
  sudo beacon install
99
99
  ```
100
100
 
101
- On macOS the plist is written under `/Library/LaunchDaemons/`, and on Linux the unit file goes under `/etc/systemd/system/`—both require root, so use `sudo`. If `sudo` cannot find `beacon` (for example with nvm), run `sudo env "PATH=$PATH" beacon install` or invoke the CLI with the full path to the global binary.
101
+ On macOS the plist is written under `/Library/LaunchDaemons/`, and on Linux the unit file goes under `/etc/systemd/system/`—both require root, so use `sudo`. The service is configured to **run as your login user** (via `SUDO_USER` when you use `sudo`), so it uses the same config directory as `beacon config`—not root’s home. If `sudo` cannot find `beacon` (for example with nvm), run `sudo env "PATH=$PATH" beacon install` or invoke the CLI with the full path to the global binary.
102
102
 
103
103
  ### Status and stats
104
104
 
@@ -110,7 +110,7 @@ beacon status
110
110
 
111
111
  Use `beacon status --json` for machine-readable output (includes `validateKeyUrl` on errors).
112
112
 
113
- After successful batches, the CLI updates **`stats.json`** next to your config (same config directory as `config.json`—for example `~/Library/Application Support/agentsoc-beacon/` on macOS). The file tracks `logsForwarded`, `batchesSucceeded`, `batchesFailed`, optional `lastError`, and `updatedAt`. If the beacon runs as another user (e.g. root), that user’s config directory holds the stats file.
113
+ After successful batches, the CLI updates **`stats.json`** next to your config (same config directory as `config.json`—for example `~/Library/Application Support/agentsoc-beacon/` on macOS). The file tracks `logsForwarded`, `batchesSucceeded`, `batchesFailed`, optional `lastError`, and `updatedAt`. The background service runs as the installing user (see Install Service above), so stats stay alongside your user config.
114
114
 
115
115
  The marketing site’s product demo terminal runs through **`beacon status`** so visitors can see a sample of this output.
116
116
 
package/dist/cli.js CHANGED
@@ -60,7 +60,8 @@ program
60
60
  await installService();
61
61
  }
62
62
  catch (err) {
63
- console.error("[beacon] Install failed:", err);
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ console.error(`[beacon] Install failed:\n\n${message}\n`);
64
65
  process.exit(1);
65
66
  }
66
67
  });
@@ -1 +1 @@
1
- {"version":3,"file":"service-install.d.ts","sourceRoot":"","sources":["../src/service-install.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,kBAgEnC"}
1
+ {"version":3,"file":"service-install.d.ts","sourceRoot":"","sources":["../src/service-install.ts"],"names":[],"mappings":"AA4IA,wBAAsB,cAAc,kBA4EnC"}
@@ -1,7 +1,131 @@
1
+ import { constants as fsConstants } from 'node:fs';
1
2
  import fs from 'node:fs/promises';
3
+ import os from 'node:os';
2
4
  import path from 'node:path';
3
- import { execSync } from 'node:child_process';
5
+ import { execFileSync, execSync } from 'node:child_process';
4
6
  import { BEACON_CONFIG_REQUIRED_MESSAGE, loadConfig } from './config.js';
7
+ const PERMISSION_DENIED_HINT = `That location is only writable as root. Run the same command with sudo, for example:
8
+
9
+ sudo beacon install
10
+
11
+ You will be prompted for your administrator password.`;
12
+ function permissionDeniedInstallError(destPath) {
13
+ return new Error(`Cannot install the system service: permission denied for ${path.dirname(destPath)}.\n\n${PERMISSION_DENIED_HINT}`);
14
+ }
15
+ async function assertServiceInstallWritable(destPath) {
16
+ const dir = path.dirname(destPath);
17
+ try {
18
+ await fs.access(dir, fsConstants.W_OK);
19
+ }
20
+ catch (e) {
21
+ const code = e && typeof e === 'object' && 'code' in e
22
+ ? e.code
23
+ : undefined;
24
+ if (code === 'EACCES' || code === 'EPERM') {
25
+ throw permissionDeniedInstallError(destPath);
26
+ }
27
+ throw e;
28
+ }
29
+ }
30
+ async function writeServiceFile(destPath, contents) {
31
+ try {
32
+ await fs.writeFile(destPath, contents, 'utf8');
33
+ }
34
+ catch (e) {
35
+ const code = e && typeof e === 'object' && 'code' in e
36
+ ? e.code
37
+ : undefined;
38
+ if (code === 'EACCES' || code === 'EPERM') {
39
+ throw permissionDeniedInstallError(destPath);
40
+ }
41
+ throw e;
42
+ }
43
+ }
44
+ /** launchd does not use your shell PATH — use an absolute interpreter path. */
45
+ function resolveLaunchdRunner() {
46
+ const isBun = process.execPath.endsWith('bun') || process.execPath.endsWith('bun.exe');
47
+ if (isBun) {
48
+ return process.execPath;
49
+ }
50
+ const base = path.basename(process.execPath);
51
+ if (/^node(\.exe)?$/i.test(base)) {
52
+ return process.execPath;
53
+ }
54
+ try {
55
+ const nodePath = execFileSync('/usr/bin/which', ['node'], {
56
+ encoding: 'utf8',
57
+ }).trim();
58
+ if (nodePath) {
59
+ return nodePath;
60
+ }
61
+ }
62
+ catch {
63
+ // fall through
64
+ }
65
+ return 'node';
66
+ }
67
+ function launchctlBootoutSystem(plistPath, label) {
68
+ try {
69
+ execFileSync('launchctl', ['bootout', 'system', plistPath], {
70
+ stdio: 'ignore',
71
+ });
72
+ return;
73
+ }
74
+ catch {
75
+ // try service target (older registration style)
76
+ }
77
+ try {
78
+ execFileSync('launchctl', ['bootout', `system/${label}`], {
79
+ stdio: 'ignore',
80
+ });
81
+ }
82
+ catch {
83
+ // not loaded yet
84
+ }
85
+ }
86
+ /**
87
+ * Config and stats paths use os.homedir(). A LaunchDaemon/systemd unit runs as root by
88
+ * default, which points at /var/root (macOS) or /root — not the user who ran `beacon config`.
89
+ * Prefer SUDO_USER from `sudo`; otherwise the current login user when not root.
90
+ */
91
+ function resolveDaemonRunAsUsername() {
92
+ const sudoUser = process.env.SUDO_USER?.trim();
93
+ if (sudoUser) {
94
+ return sudoUser;
95
+ }
96
+ const uid = typeof process.getuid === 'function' ? process.getuid() : -1;
97
+ if (uid !== 0) {
98
+ try {
99
+ return os.userInfo().username;
100
+ }
101
+ catch {
102
+ // fall through
103
+ }
104
+ }
105
+ throw new Error('Cannot determine which user account should run the beacon daemon. ' +
106
+ 'Config is stored under that user\'s home directory. ' +
107
+ 'Run install from your normal account with sudo, e.g. `sudo beacon install`, not from a root-only shell.');
108
+ }
109
+ function escapePlistString(s) {
110
+ return s
111
+ .replace(/&/g, '&')
112
+ .replace(/</g, '&lt;')
113
+ .replace(/>/g, '&gt;');
114
+ }
115
+ function launchctlBootstrapSystem(plistPath) {
116
+ try {
117
+ execFileSync('launchctl', ['bootstrap', 'system', plistPath], {
118
+ encoding: 'utf8',
119
+ stdio: ['ignore', 'inherit', 'pipe'],
120
+ });
121
+ }
122
+ catch (e) {
123
+ const err = e;
124
+ const detail = err.stderr?.toString('utf8').trim() ?? '';
125
+ const suffix = detail ? `\n${detail}` : '';
126
+ throw new Error(`launchctl bootstrap failed for ${plistPath}.${suffix}\n\nTry:\n sudo launchctl bootout system ${plistPath}\n sudo beacon install`);
127
+ }
128
+ }
5
129
  export async function installService() {
6
130
  const config = await loadConfig();
7
131
  // Service units do not inherit the install shell's env — key must be saved in config.
@@ -13,12 +137,14 @@ export async function installService() {
13
137
  const cliPath = path.resolve(process.argv[1]);
14
138
  const command = `${runner} ${cliPath} run`;
15
139
  if (process.platform === 'linux') {
140
+ const runAs = resolveDaemonRunAsUsername();
16
141
  const serviceUnit = `[Unit]
17
142
  Description=AgentSOC Syslog Beacon
18
143
  After=network.target
19
144
 
20
145
  [Service]
21
146
  Type=simple
147
+ User=${runAs}
22
148
  ExecStart=${command}
23
149
  Restart=always
24
150
  RestartSec=10
@@ -28,22 +154,28 @@ Environment=NODE_ENV=production
28
154
  WantedBy=multi-user.target
29
155
  `;
30
156
  const dest = '/etc/systemd/system/syslog-beacon.service';
31
- await fs.writeFile(dest, serviceUnit, 'utf8');
157
+ await assertServiceInstallWritable(dest);
158
+ await writeServiceFile(dest, serviceUnit);
32
159
  execSync('systemctl daemon-reload');
33
160
  execSync('systemctl enable syslog-beacon');
34
161
  execSync('systemctl start syslog-beacon');
35
162
  console.log('[syslog-beacon] Installed systemd service');
36
163
  }
37
164
  else if (process.platform === 'darwin') {
165
+ const label = 'com.agentsoc.syslog-beacon';
166
+ const runAs = escapePlistString(resolveDaemonRunAsUsername());
167
+ const launchdRunner = resolveLaunchdRunner();
38
168
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
39
169
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
40
170
  <plist version="1.0">
41
171
  <dict>
42
172
  <key>Label</key>
43
- <string>com.agentsoc.syslog-beacon</string>
173
+ <string>${label}</string>
174
+ <key>UserName</key>
175
+ <string>${runAs}</string>
44
176
  <key>ProgramArguments</key>
45
177
  <array>
46
- <string>${runner}</string>
178
+ <string>${launchdRunner}</string>
47
179
  <string>${cliPath}</string>
48
180
  <string>run</string>
49
181
  </array>
@@ -54,9 +186,13 @@ WantedBy=multi-user.target
54
186
  </dict>
55
187
  </plist>
56
188
  `;
57
- const dest = '/Library/LaunchDaemons/com.agentsoc.syslog-beacon.plist';
58
- await fs.writeFile(dest, plist, 'utf8');
59
- execSync(`launchctl load -w ${dest}`);
189
+ const dest = `/Library/LaunchDaemons/${label}.plist`;
190
+ await assertServiceInstallWritable(dest);
191
+ launchctlBootoutSystem(dest, label);
192
+ await writeServiceFile(dest, plist);
193
+ execFileSync('chown', ['root:wheel', dest], { stdio: 'inherit' });
194
+ await fs.chmod(dest, 0o644);
195
+ launchctlBootstrapSystem(dest);
60
196
  console.log('[syslog-beacon] Installed launchd service');
61
197
  }
62
198
  else if (process.platform === 'win32') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsoc/beacon",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Lightweight, background-running log forwarder (beacon) for AgentSOC",
5
5
  "type": "module",
6
6
  "bin": {