@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 +2 -2
- package/dist/cli.js +2 -1
- package/dist/service-install.d.ts.map +1 -1
- package/dist/service-install.js +143 -7
- package/package.json +1 -1
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`.
|
|
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
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"service-install.d.ts","sourceRoot":"","sources":["../src/service-install.ts"],"names":[],"mappings":"AA4IA,wBAAsB,cAAc,kBA4EnC"}
|
package/dist/service-install.js
CHANGED
|
@@ -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, '<')
|
|
113
|
+
.replace(/>/g, '>');
|
|
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
|
|
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
|
|
173
|
+
<string>${label}</string>
|
|
174
|
+
<key>UserName</key>
|
|
175
|
+
<string>${runAs}</string>
|
|
44
176
|
<key>ProgramArguments</key>
|
|
45
177
|
<array>
|
|
46
|
-
<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 =
|
|
58
|
-
await
|
|
59
|
-
|
|
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') {
|