@forgezero/agent 0.1.26 → 0.1.28
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 +5 -3
- package/dist/agent-heartbeat.d.ts +45 -0
- package/dist/agent-heartbeat.js +511 -0
- package/dist/agent-update-helper.d.ts +45 -0
- package/dist/agent-update-helper.js +366 -0
- package/dist/agent-update.d.ts +46 -0
- package/dist/agent-update.js +184 -0
- package/dist/definition.d.ts +1 -7
- package/dist/definition.js +92 -23
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment.d.ts +3 -0
- package/dist/fz-agent.js +7865 -2617
- package/dist/fz.js +8287 -38
- package/dist/guest-enrolment.js +14 -1
- package/dist/index.d.ts +13 -0
- package/dist/metal-helper-socket.js +23 -4
- package/dist/metal-provision.js +23 -4
- package/dist/migration-pull.js +14 -1
- package/dist/node-vault.js +15 -2
- package/dist/provision.d.ts +13 -0
- package/dist/provision.js +685 -6
- package/dist/provisioning-pull.js +14 -1
- package/dist/signed-node-http.d.ts +10 -0
- package/dist/socket.d.ts +2 -0
- package/dist/software-helper.d.ts +14 -0
- package/dist/software-helper.js +203 -0
- package/dist/software.d.ts +27 -0
- package/dist/software.js +95 -0
- package/dist/version.d.ts +1 -1
- package/package.json +24 -4
|
@@ -15,8 +15,21 @@ class SignedNodeHttpError extends Error {
|
|
|
15
15
|
this.name = "SignedNodeHttpError";
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
function signedNodeApiUrl(value) {
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(value);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error("Agent API URL must be an absolute HTTPS URL.");
|
|
24
|
+
}
|
|
25
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
26
|
+
if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.search || url.hash) {
|
|
27
|
+
throw new Error("Agent API URL must be HTTPS without credentials, query or fragment.");
|
|
28
|
+
}
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
18
31
|
async function postSignedNode(options, path, body) {
|
|
19
|
-
const url =
|
|
32
|
+
const url = signedNodeApiUrl(options.apiUrl);
|
|
20
33
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
21
34
|
url.search = "";
|
|
22
35
|
url.hash = "";
|
|
@@ -10,5 +10,15 @@ export declare class SignedNodeHttpError extends Error {
|
|
|
10
10
|
readonly status: number;
|
|
11
11
|
constructor(status: number, message: string);
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate the one origin every outbound Agent client shares.
|
|
15
|
+
*
|
|
16
|
+
* Hybrid request authentication and a sealed response protect the payload, but
|
|
17
|
+
* they do not hide routing metadata. Accepting plain HTTP for a remote API
|
|
18
|
+
* would still expose node identity, operation timing and ciphertext to an
|
|
19
|
+
* on-path observer. Loopback HTTP remains available for local development and
|
|
20
|
+
* for a supervised same-host API; production traffic must use HTTPS.
|
|
21
|
+
*/
|
|
22
|
+
export declare function signedNodeApiUrl(value: string): URL;
|
|
13
23
|
/** One implementation of the hybrid-signed machine HTTP contract. */
|
|
14
24
|
export declare function postSignedNode<T>(options: SignedNodeHttpOptions, path: string, body: object): Promise<T>;
|
package/dist/socket.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface AttestationSource {
|
|
|
113
113
|
}
|
|
114
114
|
export interface AgentOptions {
|
|
115
115
|
socketPath: string;
|
|
116
|
+
/** PID 1-owned listener retained while the Agent drains and restarts. */
|
|
117
|
+
listenFd?: number;
|
|
116
118
|
keys: NodeKeyPair;
|
|
117
119
|
/** The node's identifier, as the platform knows it. */
|
|
118
120
|
nodeKey: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Server } from 'node:net';
|
|
2
|
+
import { ensureSoftwareRequirements, type SoftwareRequirement } from './software';
|
|
3
|
+
export declare const DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
4
|
+
export declare const SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
5
|
+
export declare const SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
|
|
6
|
+
export declare function startSoftwareHelper(options?: {
|
|
7
|
+
socketPath?: string;
|
|
8
|
+
ensure?: typeof ensureSoftwareRequirements;
|
|
9
|
+
}): Server;
|
|
10
|
+
export declare function requestSoftware(requirements: readonly SoftwareRequirement[], socketPath?: string, timeoutMs?: number): Promise<Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
version: string;
|
|
13
|
+
changed: boolean;
|
|
14
|
+
}>>;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// src/software.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4
|
+
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
5
|
+
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
6
|
+
var UBUNTU_2604_X64 = [
|
|
7
|
+
{
|
|
8
|
+
requirement: { id: "bun", version: "1.3.14" },
|
|
9
|
+
check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
|
|
10
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
requirement: { id: "nginx", version: "ubuntu-26.04" },
|
|
14
|
+
check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
|
|
15
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
requirement: { id: "arangodb", version: "3.11.14" },
|
|
19
|
+
check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14'`,
|
|
20
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
requirement: { id: "cloudflared", version: "2026.7.3" },
|
|
24
|
+
check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
|
|
25
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
requirement: { id: "ufw", version: "ubuntu-26.04" },
|
|
29
|
+
check: "command -v ufw >/dev/null",
|
|
30
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
34
|
+
const values = Object.fromEntries(osRelease.split(`
|
|
35
|
+
`).flatMap((line) => {
|
|
36
|
+
const separator = line.indexOf("=");
|
|
37
|
+
return separator > 0 ? [[line.slice(0, separator), line.slice(separator + 1).replace(/^['"]|['"]$/g, "")]] : [];
|
|
38
|
+
}));
|
|
39
|
+
return {
|
|
40
|
+
os: { id: (values.ID ?? "unknown").toLowerCase(), versionId: values.VERSION_ID ?? "unknown" },
|
|
41
|
+
architecture
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function validateSoftwareRequirements(value) {
|
|
45
|
+
if (!Array.isArray(value) || value.length > 32)
|
|
46
|
+
throw new Error("software requirements must be an array of at most 32 entries");
|
|
47
|
+
const seen = new Set;
|
|
48
|
+
return value.map((item) => {
|
|
49
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
50
|
+
throw new Error("software requirement must be an object");
|
|
51
|
+
const row = item;
|
|
52
|
+
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
53
|
+
throw new Error("software requirement contains an unknown field");
|
|
54
|
+
}
|
|
55
|
+
if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
56
|
+
throw new Error("software requirement coordinate is invalid");
|
|
57
|
+
}
|
|
58
|
+
const requirement = { id: row.id, version: row.version };
|
|
59
|
+
if (seen.has(requirement.id))
|
|
60
|
+
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
61
|
+
seen.add(requirement.id);
|
|
62
|
+
return requirement;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
66
|
+
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
67
|
+
const observation = options.observation ?? observeSoftwareHost();
|
|
68
|
+
if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
|
|
69
|
+
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
70
|
+
}
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const requirement of requirements) {
|
|
73
|
+
const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
74
|
+
if (!strategy)
|
|
75
|
+
throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
|
|
76
|
+
const before = await options.exec(strategy.check);
|
|
77
|
+
if (before.exitCode === 0) {
|
|
78
|
+
results.push({ ...requirement, changed: false });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const installed = await options.exec(strategy.install);
|
|
82
|
+
if (installed.exitCode !== 0)
|
|
83
|
+
throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
|
|
84
|
+
const after = await options.exec(strategy.check);
|
|
85
|
+
if (after.exitCode !== 0)
|
|
86
|
+
throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
|
|
87
|
+
results.push({ ...requirement, changed: true });
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/software-helper.ts
|
|
93
|
+
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
94
|
+
import { connect, createServer } from "node:net";
|
|
95
|
+
import { dirname } from "node:path";
|
|
96
|
+
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
97
|
+
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
98
|
+
var SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
|
|
99
|
+
var MAX_REQUEST_BYTES = 8 * 1024;
|
|
100
|
+
var MAX_PENDING_REQUESTS = 128;
|
|
101
|
+
var execute = async (command) => {
|
|
102
|
+
const child = Bun.spawn(["/bin/bash", "-Eeuo", "pipefail", "-c", command], {
|
|
103
|
+
stdout: "pipe",
|
|
104
|
+
stderr: "pipe",
|
|
105
|
+
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
|
|
106
|
+
});
|
|
107
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
108
|
+
new Response(child.stdout).text(),
|
|
109
|
+
new Response(child.stderr).text(),
|
|
110
|
+
child.exited
|
|
111
|
+
]);
|
|
112
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
113
|
+
};
|
|
114
|
+
function startSoftwareHelper(options = {}) {
|
|
115
|
+
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
116
|
+
if (existsSync(socketPath))
|
|
117
|
+
unlinkSync(socketPath);
|
|
118
|
+
mkdirSync(dirname(socketPath), { recursive: true, mode: 488 });
|
|
119
|
+
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
120
|
+
let tail = Promise.resolve();
|
|
121
|
+
let pending = 0;
|
|
122
|
+
const server = createServer((socket) => {
|
|
123
|
+
let buffer = "";
|
|
124
|
+
socket.on("data", (chunk) => {
|
|
125
|
+
buffer += chunk.toString("utf8");
|
|
126
|
+
if (Buffer.byteLength(buffer) > MAX_REQUEST_BYTES)
|
|
127
|
+
return socket.destroy();
|
|
128
|
+
const newline = buffer.indexOf(`
|
|
129
|
+
`);
|
|
130
|
+
if (newline < 0)
|
|
131
|
+
return;
|
|
132
|
+
const line = buffer.slice(0, newline);
|
|
133
|
+
buffer = "";
|
|
134
|
+
if (pending >= MAX_PENDING_REQUESTS) {
|
|
135
|
+
socket.end(`${JSON.stringify({
|
|
136
|
+
ok: false,
|
|
137
|
+
error: { code: "SOFTWARE_BUSY", message: "software helper queue is full" }
|
|
138
|
+
})}
|
|
139
|
+
`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
pending += 1;
|
|
143
|
+
const work = tail.then(() => Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
|
|
144
|
+
if (request.op !== "ensure")
|
|
145
|
+
throw new Error("unknown software helper operation");
|
|
146
|
+
const requirements = validateSoftwareRequirements(request.requirements);
|
|
147
|
+
const results = await ensure(requirements, { exec: execute });
|
|
148
|
+
socket.end(`${JSON.stringify({ ok: true, results })}
|
|
149
|
+
`);
|
|
150
|
+
}).catch((cause) => socket.end(`${JSON.stringify({
|
|
151
|
+
ok: false,
|
|
152
|
+
error: { code: "SOFTWARE_REFUSED", message: cause instanceof Error ? cause.message : String(cause) }
|
|
153
|
+
})}
|
|
154
|
+
`)).finally(() => {
|
|
155
|
+
pending -= 1;
|
|
156
|
+
}));
|
|
157
|
+
tail = work.then(() => {
|
|
158
|
+
return;
|
|
159
|
+
}, () => {
|
|
160
|
+
return;
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
socket.on("error", () => socket.destroy());
|
|
164
|
+
});
|
|
165
|
+
server.listen(socketPath, () => chmodSync(socketPath, 432));
|
|
166
|
+
return server;
|
|
167
|
+
}
|
|
168
|
+
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
169
|
+
validateSoftwareRequirements(requirements);
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
172
|
+
`));
|
|
173
|
+
let buffer = "";
|
|
174
|
+
socket.setTimeout(timeoutMs, () => {
|
|
175
|
+
socket.destroy();
|
|
176
|
+
reject(new Error("software helper did not answer before its deadline"));
|
|
177
|
+
});
|
|
178
|
+
socket.on("data", (chunk) => {
|
|
179
|
+
buffer += chunk.toString("utf8");
|
|
180
|
+
const newline = buffer.indexOf(`
|
|
181
|
+
`);
|
|
182
|
+
if (newline < 0)
|
|
183
|
+
return;
|
|
184
|
+
socket.end();
|
|
185
|
+
try {
|
|
186
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
187
|
+
if (!response.ok || !response.results)
|
|
188
|
+
throw new Error(response.error?.message ?? "software helper refused the request");
|
|
189
|
+
resolve(response.results);
|
|
190
|
+
} catch (cause) {
|
|
191
|
+
reject(cause);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
socket.on("error", reject);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
startSoftwareHelper,
|
|
199
|
+
requestSoftware,
|
|
200
|
+
SOFTWARE_HELPER_UNIT_PATH,
|
|
201
|
+
SOFTWARE_HELPER_GROUP,
|
|
202
|
+
DEFAULT_SOFTWARE_HELPER_SOCKET
|
|
203
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Repository input is a catalogue coordinate, never a root command. */
|
|
2
|
+
export interface SoftwareRequirement {
|
|
3
|
+
id: 'bun' | 'nginx' | 'arangodb' | 'cloudflared' | 'ufw';
|
|
4
|
+
version: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SoftwareObservation {
|
|
7
|
+
os: {
|
|
8
|
+
id: string;
|
|
9
|
+
versionId: string;
|
|
10
|
+
};
|
|
11
|
+
architecture: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SoftwareCommandResult {
|
|
14
|
+
exitCode: number;
|
|
15
|
+
output: string;
|
|
16
|
+
}
|
|
17
|
+
export type SoftwareExec = (command: string) => Promise<SoftwareCommandResult>;
|
|
18
|
+
export declare function observeSoftwareHost(osRelease?: string, architecture?: NodeJS.Architecture): SoftwareObservation;
|
|
19
|
+
export declare function validateSoftwareRequirements(value: unknown): SoftwareRequirement[];
|
|
20
|
+
export declare function ensureSoftwareRequirements(requirementsInput: unknown, options: {
|
|
21
|
+
observation?: SoftwareObservation;
|
|
22
|
+
exec: SoftwareExec;
|
|
23
|
+
}): Promise<Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
version: string;
|
|
26
|
+
changed: boolean;
|
|
27
|
+
}>>;
|
package/dist/software.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/software.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4
|
+
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
5
|
+
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
6
|
+
var UBUNTU_2604_X64 = [
|
|
7
|
+
{
|
|
8
|
+
requirement: { id: "bun", version: "1.3.14" },
|
|
9
|
+
check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
|
|
10
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
requirement: { id: "nginx", version: "ubuntu-26.04" },
|
|
14
|
+
check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
|
|
15
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
requirement: { id: "arangodb", version: "3.11.14" },
|
|
19
|
+
check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14'`,
|
|
20
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
requirement: { id: "cloudflared", version: "2026.7.3" },
|
|
24
|
+
check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
|
|
25
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
requirement: { id: "ufw", version: "ubuntu-26.04" },
|
|
29
|
+
check: "command -v ufw >/dev/null",
|
|
30
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
34
|
+
const values = Object.fromEntries(osRelease.split(`
|
|
35
|
+
`).flatMap((line) => {
|
|
36
|
+
const separator = line.indexOf("=");
|
|
37
|
+
return separator > 0 ? [[line.slice(0, separator), line.slice(separator + 1).replace(/^['"]|['"]$/g, "")]] : [];
|
|
38
|
+
}));
|
|
39
|
+
return {
|
|
40
|
+
os: { id: (values.ID ?? "unknown").toLowerCase(), versionId: values.VERSION_ID ?? "unknown" },
|
|
41
|
+
architecture
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function validateSoftwareRequirements(value) {
|
|
45
|
+
if (!Array.isArray(value) || value.length > 32)
|
|
46
|
+
throw new Error("software requirements must be an array of at most 32 entries");
|
|
47
|
+
const seen = new Set;
|
|
48
|
+
return value.map((item) => {
|
|
49
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
50
|
+
throw new Error("software requirement must be an object");
|
|
51
|
+
const row = item;
|
|
52
|
+
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
53
|
+
throw new Error("software requirement contains an unknown field");
|
|
54
|
+
}
|
|
55
|
+
if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
56
|
+
throw new Error("software requirement coordinate is invalid");
|
|
57
|
+
}
|
|
58
|
+
const requirement = { id: row.id, version: row.version };
|
|
59
|
+
if (seen.has(requirement.id))
|
|
60
|
+
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
61
|
+
seen.add(requirement.id);
|
|
62
|
+
return requirement;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
66
|
+
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
67
|
+
const observation = options.observation ?? observeSoftwareHost();
|
|
68
|
+
if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
|
|
69
|
+
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
70
|
+
}
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const requirement of requirements) {
|
|
73
|
+
const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
74
|
+
if (!strategy)
|
|
75
|
+
throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
|
|
76
|
+
const before = await options.exec(strategy.check);
|
|
77
|
+
if (before.exitCode === 0) {
|
|
78
|
+
results.push({ ...requirement, changed: false });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const installed = await options.exec(strategy.install);
|
|
82
|
+
if (installed.exitCode !== 0)
|
|
83
|
+
throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
|
|
84
|
+
const after = await options.exec(strategy.check);
|
|
85
|
+
if (after.exitCode !== 0)
|
|
86
|
+
throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
|
|
87
|
+
results.push({ ...requirement, changed: true });
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
validateSoftwareRequirements,
|
|
93
|
+
observeSoftwareHost,
|
|
94
|
+
ensureSoftwareRequirements
|
|
95
|
+
};
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.28";
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
|
|
3
3
|
"name": "@forgezero/agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.28",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"check": "tsc --noEmit",
|
|
8
8
|
"prebuild": "rm -rf dist",
|
|
9
|
-
"build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm
|
|
9
|
+
"build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm && bun build src/cli/index.ts --outfile dist/fz.js --target bun --format esm && bun build src/compute.ts src/provision.ts src/subscribe.ts src/pipeline.ts src/definition.ts src/ssh-server.ts src/ssh-listen.ts src/provisioning-pull.ts src/migration-pull.ts src/guest-enrolment.ts src/node-vault.ts src/metal-provision.ts src/metal-helper-socket.ts src/lifecycle-helper.ts src/deployment-runner.ts src/agent-update.ts src/agent-update-helper.ts src/agent-heartbeat.ts src/software.ts src/software-helper.ts src/ubuntu.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
|
|
10
10
|
"prepublishOnly": "bun run check && bun run build"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
+
"@forgezero/access": "^0.1.0",
|
|
13
14
|
"typescript": "^5.6.0",
|
|
14
15
|
"@types/bun": "latest",
|
|
15
16
|
"@types/node": "^22.0.0",
|
|
@@ -17,9 +18,8 @@
|
|
|
17
18
|
"@noble/post-quantum": "^0.6.1"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@forgezero/access": "^0.1.0",
|
|
21
21
|
"@forgezero/runtime": "^0.1.4",
|
|
22
|
-
"@forgezero/vault": "^0.1.
|
|
22
|
+
"@forgezero/vault": "^0.1.7",
|
|
23
23
|
"@noble/curves": "^2.2.0",
|
|
24
24
|
"@noble/hashes": "^2.2.0",
|
|
25
25
|
"@noble/post-quantum": "^0.6.1",
|
|
@@ -96,6 +96,26 @@
|
|
|
96
96
|
"types": "./dist/lifecycle-helper.d.ts",
|
|
97
97
|
"default": "./dist/lifecycle-helper.js"
|
|
98
98
|
},
|
|
99
|
+
"./agent-update": {
|
|
100
|
+
"types": "./dist/agent-update.d.ts",
|
|
101
|
+
"default": "./dist/agent-update.js"
|
|
102
|
+
},
|
|
103
|
+
"./agent-update-helper": {
|
|
104
|
+
"types": "./dist/agent-update-helper.d.ts",
|
|
105
|
+
"default": "./dist/agent-update-helper.js"
|
|
106
|
+
},
|
|
107
|
+
"./agent-heartbeat": {
|
|
108
|
+
"types": "./dist/agent-heartbeat.d.ts",
|
|
109
|
+
"default": "./dist/agent-heartbeat.js"
|
|
110
|
+
},
|
|
111
|
+
"./software": {
|
|
112
|
+
"types": "./dist/software.d.ts",
|
|
113
|
+
"default": "./dist/software.js"
|
|
114
|
+
},
|
|
115
|
+
"./software-helper": {
|
|
116
|
+
"types": "./dist/software-helper.d.ts",
|
|
117
|
+
"default": "./dist/software-helper.js"
|
|
118
|
+
},
|
|
99
119
|
"./metal-provision": {
|
|
100
120
|
"types": "./dist/metal-provision.d.ts",
|
|
101
121
|
"default": "./dist/metal-provision.js"
|