@nettee/beacon 0.1.8 → 0.1.9
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 +20 -1
- package/dist/config/global.js +1 -0
- package/dist/config/runtime-environment.js +91 -0
- package/dist/doctor.js +3 -0
- package/dist/manual-trigger.js +3 -0
- package/dist/run/create-pi-orchestrator.js +1 -0
- package/dist/runtime/pi-rpc.js +8 -2
- package/dist/schedule-trigger.js +3 -0
- package/dist/service.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ Copy the files under [`examples`](examples) into a private Beacon home and repla
|
|
|
48
48
|
```text
|
|
49
49
|
~/.beacon/
|
|
50
50
|
├── config.yaml
|
|
51
|
+
├── runtime.env
|
|
51
52
|
├── secrets.json
|
|
52
53
|
└── profiles/
|
|
53
54
|
└── example/
|
|
@@ -59,9 +60,27 @@ Protect the home and secret file before running Beacon:
|
|
|
59
60
|
|
|
60
61
|
```sh
|
|
61
62
|
chmod 700 /Users/USERNAME/.beacon
|
|
63
|
+
test ! -e /Users/USERNAME/.beacon/runtime.env || chmod 600 /Users/USERNAME/.beacon/runtime.env
|
|
62
64
|
chmod 600 /Users/USERNAME/.beacon/secrets.json
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
`runtime.env` is optional. When present, Beacon loads every `KEY=VALUE` entry
|
|
68
|
+
once at startup and passes it to every Pi Run. This gives launchd deployments a
|
|
69
|
+
deterministic runtime environment without executing interactive shell files such
|
|
70
|
+
as `.zshrc`:
|
|
71
|
+
|
|
72
|
+
```dotenv
|
|
73
|
+
GRAFANA_READER_TOKEN_PROD=replace-me
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The file must be owned by the Beacon user with mode `0600`. Blank lines and
|
|
77
|
+
lines beginning with `#` are ignored; values are literal and never evaluated as
|
|
78
|
+
shell syntax. Duplicate or invalid names, malformed entries, and reserved
|
|
79
|
+
process variables such as `PATH`, `NODE_OPTIONS`, `BEACON_*`, and `FEISHU_*`
|
|
80
|
+
fail startup. A missing file is treated as an empty runtime environment. Changes
|
|
81
|
+
take effect after Beacon restarts. Beacon does not log or persist values from
|
|
82
|
+
this file, but every configured Profile and Pi Run receives them.
|
|
83
|
+
|
|
65
84
|
Configure a dedicated absolute Pi session root in `config.yaml`:
|
|
66
85
|
|
|
67
86
|
```yaml
|
|
@@ -76,7 +95,7 @@ created by Beacon 0.1.2 and earlier; when omitted it defaults to `sessions/`
|
|
|
76
95
|
beside `config.yaml`. When configured, it must be absolute. Beacon creates
|
|
77
96
|
per-Run directories with mode `0700` when Pi starts.
|
|
78
97
|
|
|
79
|
-
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, unsafe Prompt paths, permissive secret permissions, and missing Profile credentials all fail startup. Beacon validates all Profiles before opening a Feishu connection.
|
|
98
|
+
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, unsafe Prompt paths, permissive secret or runtime-environment permissions, and missing Profile credentials all fail startup. Beacon validates all Profiles before opening a Feishu connection.
|
|
80
99
|
|
|
81
100
|
Each Schedule uses a five-field cron expression and an IANA timezone. Its `delivery.chat_id` may identify either a direct chat or a group chat; Beacon deliberately does not infer or fall back to another destination. A newly discovered Schedule starts at the current time. After sleep or restart, overdue occurrences are reconciled and coalesced to the most recent one.
|
|
82
101
|
|
package/dist/config/global.js
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
const variableNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
|
+
const reservedVariableNames = new Set([
|
|
4
|
+
"PATH",
|
|
5
|
+
"HOME",
|
|
6
|
+
"USER",
|
|
7
|
+
"LOGNAME",
|
|
8
|
+
"SHELL",
|
|
9
|
+
"TMPDIR",
|
|
10
|
+
"LANG",
|
|
11
|
+
"LC_ALL",
|
|
12
|
+
"TERM",
|
|
13
|
+
"COLORTERM",
|
|
14
|
+
"SSH_AUTH_SOCK",
|
|
15
|
+
"XDG_CONFIG_HOME",
|
|
16
|
+
"XDG_CACHE_HOME",
|
|
17
|
+
"GH_CONFIG_DIR",
|
|
18
|
+
"NODE_OPTIONS",
|
|
19
|
+
"PI_CODING_AGENT_DIR",
|
|
20
|
+
"PI_CODING_AGENT_SESSION_DIR",
|
|
21
|
+
"PI_PACKAGE_DIR",
|
|
22
|
+
"PI_OFFLINE",
|
|
23
|
+
"PI_TELEMETRY",
|
|
24
|
+
]);
|
|
25
|
+
export function assertRuntimeEnvironmentKey(key) {
|
|
26
|
+
if (!variableNamePattern.test(key)) {
|
|
27
|
+
throw new Error(`Invalid runtime environment variable name: ${key}`);
|
|
28
|
+
}
|
|
29
|
+
if (reservedVariableNames.has(key) ||
|
|
30
|
+
key.startsWith("BEACON_") ||
|
|
31
|
+
key.startsWith("FEISHU_")) {
|
|
32
|
+
throw new Error(`Runtime environment variable is reserved: ${key}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseRuntimeEnvironment(raw, path) {
|
|
36
|
+
const environment = {};
|
|
37
|
+
const lines = raw.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
38
|
+
for (const [index, line] of lines.entries()) {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
41
|
+
continue;
|
|
42
|
+
const separator = line.indexOf("=");
|
|
43
|
+
if (separator < 0) {
|
|
44
|
+
throw new Error(`Invalid runtime environment entry at ${path}:${index + 1}: expected KEY=VALUE`);
|
|
45
|
+
}
|
|
46
|
+
const key = line.slice(0, separator).trim();
|
|
47
|
+
assertRuntimeEnvironmentKey(key);
|
|
48
|
+
if (Object.hasOwn(environment, key)) {
|
|
49
|
+
throw new Error(`Duplicate runtime environment variable at ${path}:${index + 1}: ${key}`);
|
|
50
|
+
}
|
|
51
|
+
environment[key] = line.slice(separator + 1).trim();
|
|
52
|
+
}
|
|
53
|
+
return environment;
|
|
54
|
+
}
|
|
55
|
+
export async function loadRuntimeEnvironment(path) {
|
|
56
|
+
let metadata;
|
|
57
|
+
try {
|
|
58
|
+
metadata = await stat(path);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (typeof error === "object" &&
|
|
62
|
+
error !== null &&
|
|
63
|
+
"code" in error &&
|
|
64
|
+
error.code === "ENOENT") {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Cannot inspect Beacon runtime environment file at ${path}`, {
|
|
68
|
+
cause: error,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!metadata.isFile()) {
|
|
72
|
+
throw new Error(`Beacon runtime environment path is not a file: ${path}`);
|
|
73
|
+
}
|
|
74
|
+
if ((metadata.mode & 0o777) !== 0o600) {
|
|
75
|
+
throw new Error(`Beacon runtime environment file must have mode 0600: ${path}`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof process.getuid === "function" &&
|
|
78
|
+
metadata.uid !== process.getuid()) {
|
|
79
|
+
throw new Error(`Beacon runtime environment file must be owned by the current user: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = await readFile(path, "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`Cannot read Beacon runtime environment file at ${path}`, {
|
|
87
|
+
cause: error,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return parseRuntimeEnvironment(raw, path);
|
|
91
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { loadGlobalConfig } from "./config/global.js";
|
|
2
2
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
3
|
+
import { loadRuntimeEnvironment } from "./config/runtime-environment.js";
|
|
3
4
|
import { loadFeishuCredentials } from "./config/secrets.js";
|
|
4
5
|
import { createFeishuGateway } from "./feishu/gateway.js";
|
|
5
6
|
import { runPiAgent } from "./runtime/pi-rpc.js";
|
|
6
7
|
export async function runDoctor(configPath) {
|
|
7
8
|
const global = await loadGlobalConfig(configPath);
|
|
9
|
+
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
8
10
|
const profiles = await loadProfileRegistry(global.profilesDirectory);
|
|
9
11
|
for (const profile of profiles) {
|
|
10
12
|
const credentials = await loadFeishuCredentials(profile.id, global.secretsPath);
|
|
@@ -20,6 +22,7 @@ export async function runDoctor(configPath) {
|
|
|
20
22
|
timeoutMs: global.runs.timeoutSeconds * 1_000,
|
|
21
23
|
terminateGraceMs: global.runs.terminateGraceSeconds * 1_000,
|
|
22
24
|
environment: { PI_CODING_AGENT_DIR: global.pi.codingAgentDirectory },
|
|
25
|
+
runtimeEnvironment,
|
|
23
26
|
});
|
|
24
27
|
if (result.text !== "BEACON_PI_RPC_OK") {
|
|
25
28
|
throw new Error(`Pi readiness returned unexpected text for Profile ${profile.id}: ${JSON.stringify(result.text)}`);
|
package/dist/manual-trigger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadGlobalConfig } from "./config/global.js";
|
|
3
3
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
4
|
+
import { loadRuntimeEnvironment } from "./config/runtime-environment.js";
|
|
4
5
|
import { renderFinalOutcomeAsText } from "./outcome/content.js";
|
|
5
6
|
import { startOutcomeServer } from "./outcome/server.js";
|
|
6
7
|
import { createPiRunOrchestrator } from "./run/create-pi-orchestrator.js";
|
|
@@ -15,6 +16,7 @@ export async function runManualTrigger(configPath, profileId, input, output = wr
|
|
|
15
16
|
if (!input.trim())
|
|
16
17
|
throw new Error("Manual Trigger input must not be empty");
|
|
17
18
|
const global = await loadGlobalConfig(configPath);
|
|
19
|
+
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
18
20
|
const profiles = await loadProfileRegistry(global.profilesDirectory);
|
|
19
21
|
const profile = profiles.find((candidate) => candidate.id === profileId);
|
|
20
22
|
if (!profile)
|
|
@@ -38,6 +40,7 @@ export async function runManualTrigger(configPath, profileId, input, output = wr
|
|
|
38
40
|
};
|
|
39
41
|
const orchestrator = createPiRunOrchestrator({
|
|
40
42
|
config: global,
|
|
43
|
+
runtimeEnvironment,
|
|
41
44
|
profile,
|
|
42
45
|
store,
|
|
43
46
|
queue: new RunQueue(global.runs.maxConcurrent, global.runs.maxQueued),
|
package/dist/runtime/pi-rpc.js
CHANGED
|
@@ -3,6 +3,7 @@ import { chmod, mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertRuntimeEnvironmentKey } from "../config/runtime-environment.js";
|
|
6
7
|
export class PiRuntimeError extends Error {
|
|
7
8
|
code;
|
|
8
9
|
constructor(code, message, options) {
|
|
@@ -86,13 +87,18 @@ const inheritedEnvironmentKeys = [
|
|
|
86
87
|
"PI_OFFLINE",
|
|
87
88
|
"PI_TELEMETRY",
|
|
88
89
|
];
|
|
89
|
-
function buildPiEnvironment(outcome, configured = {}) {
|
|
90
|
+
function buildPiEnvironment(outcome, configured = {}, runtimeEnvironment = {}) {
|
|
90
91
|
const environment = {};
|
|
91
92
|
for (const key of inheritedEnvironmentKeys) {
|
|
92
93
|
const value = process.env[key];
|
|
93
94
|
if (value !== undefined)
|
|
94
95
|
environment[key] = value;
|
|
95
96
|
}
|
|
97
|
+
for (const [key, value] of Object.entries(runtimeEnvironment)) {
|
|
98
|
+
assertRuntimeEnvironmentKey(key);
|
|
99
|
+
if (value !== undefined)
|
|
100
|
+
environment[key] = value;
|
|
101
|
+
}
|
|
96
102
|
for (const [key, value] of Object.entries(configured)) {
|
|
97
103
|
if (!inheritedEnvironmentKeys.includes(key)) {
|
|
98
104
|
throw new Error(`Pi environment key is not allowlisted: ${key}`);
|
|
@@ -167,7 +173,7 @@ export async function runPiAgent(request, options = {}) {
|
|
|
167
173
|
}
|
|
168
174
|
const child = spawn(executable, buildArguments(request), {
|
|
169
175
|
cwd: request.workspace,
|
|
170
|
-
env: buildPiEnvironment(request.outcome, options.environment),
|
|
176
|
+
env: buildPiEnvironment(request.outcome, options.environment, options.runtimeEnvironment),
|
|
171
177
|
stdio: ["pipe", "pipe", "pipe"],
|
|
172
178
|
});
|
|
173
179
|
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
package/dist/schedule-trigger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadGlobalConfig } from "./config/global.js";
|
|
3
3
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
4
|
+
import { loadRuntimeEnvironment } from "./config/runtime-environment.js";
|
|
4
5
|
import { loadFeishuCredentials } from "./config/secrets.js";
|
|
5
6
|
import { createFeishuGateway } from "./feishu/gateway.js";
|
|
6
7
|
import { startOutcomeServer } from "./outcome/server.js";
|
|
@@ -50,6 +51,7 @@ export async function triggerScheduleOnce(options) {
|
|
|
50
51
|
}
|
|
51
52
|
export async function runScheduleTrigger(configPath, profileId, scheduleId) {
|
|
52
53
|
const global = await loadGlobalConfig(configPath);
|
|
54
|
+
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
53
55
|
const profiles = await loadProfileRegistry(global.profilesDirectory);
|
|
54
56
|
const profile = profiles.find((candidate) => candidate.id === profileId);
|
|
55
57
|
if (!profile)
|
|
@@ -61,6 +63,7 @@ export async function runScheduleTrigger(configPath, profileId, scheduleId) {
|
|
|
61
63
|
const store = new TriggerStore(profile.directory, profile.id);
|
|
62
64
|
const orchestrator = createPiRunOrchestrator({
|
|
63
65
|
config: global,
|
|
66
|
+
runtimeEnvironment,
|
|
64
67
|
profile,
|
|
65
68
|
store,
|
|
66
69
|
queue: new RunQueue(global.runs.maxConcurrent, global.runs.maxQueued),
|
package/dist/service.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadGlobalConfig } from "./config/global.js";
|
|
2
2
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
3
|
+
import { loadRuntimeEnvironment } from "./config/runtime-environment.js";
|
|
3
4
|
import { loadFeishuCredentials } from "./config/secrets.js";
|
|
4
5
|
import { createFeishuGateway } from "./feishu/gateway.js";
|
|
5
6
|
import { createFeishuMessagePipeline } from "./feishu/message-pipeline.js";
|
|
@@ -29,6 +30,7 @@ function shutdownController() {
|
|
|
29
30
|
}
|
|
30
31
|
export async function runBeacon(configPath) {
|
|
31
32
|
const global = await loadGlobalConfig(configPath);
|
|
33
|
+
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
32
34
|
const profiles = await loadProfileRegistry(global.profilesDirectory);
|
|
33
35
|
const credentials = await Promise.all(profiles.map((profile) => loadFeishuCredentials(profile.id, global.secretsPath)));
|
|
34
36
|
const outcomes = await startOutcomeServer();
|
|
@@ -46,6 +48,7 @@ export async function runBeacon(configPath) {
|
|
|
46
48
|
const store = new TriggerStore(profile.directory, profile.id);
|
|
47
49
|
const orchestrator = createPiRunOrchestrator({
|
|
48
50
|
config: global,
|
|
51
|
+
runtimeEnvironment,
|
|
49
52
|
profile,
|
|
50
53
|
store,
|
|
51
54
|
queue,
|