@forgezero/agent 0.1.53 → 0.1.56
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 +118 -9
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +110 -69
- package/dist/community-rehearsal-host.d.ts +7 -1
- package/dist/community-rehearsal-host.js +64 -6
- package/dist/container-supervisor.d.ts +36 -0
- package/dist/definition.js +37 -5
- package/dist/deploy-actions.d.ts +87 -0
- package/dist/deploy-compiler.d.ts +26 -0
- package/dist/deploy-compiler.js +1050 -0
- package/dist/deploy-file.js +44 -12
- package/dist/deploy-plan-runner.d.ts +65 -0
- package/dist/deploy-plan-runner.js +1205 -0
- package/dist/deploy-plan.d.ts +41 -0
- package/dist/deploy-plan.js +925 -0
- package/dist/deploy-providers.d.ts +85 -0
- package/dist/deploy.d.ts +439 -0
- package/dist/deploy.js +169 -0
- package/dist/deployment.d.ts +5 -0
- package/dist/fz-agent.js +2137 -326
- package/dist/fz.js +1492 -320
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +212 -130
- package/dist/platform-bootstrap-runtime.js +1 -1
- package/dist/platform-fleet-verification.js +499 -120
- package/dist/platform-genesis.d.ts +9 -0
- package/dist/provision.js +399 -20
- package/dist/software-helper.d.ts +5 -0
- package/dist/software-helper.js +399 -18
- package/dist/software.d.ts +1 -1
- package/dist/software.js +37 -5
- package/dist/version.d.ts +1 -1
- package/package.json +17 -1
package/dist/software-helper.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
accessSync,
|
|
5
5
|
chmodSync,
|
|
6
6
|
copyFileSync,
|
|
7
|
+
existsSync,
|
|
7
8
|
mkdtempSync,
|
|
8
9
|
mkdirSync,
|
|
9
10
|
readFileSync,
|
|
@@ -24,6 +25,7 @@ var OS_CATALOG = [
|
|
|
24
25
|
];
|
|
25
26
|
var SOFTWARE_CATALOG = [
|
|
26
27
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
28
|
+
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
27
29
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
28
30
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
29
31
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -33,6 +35,7 @@ var SOFTWARE_CATALOG = [
|
|
|
33
35
|
];
|
|
34
36
|
var UBUNTU_2604_X64 = [
|
|
35
37
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
38
|
+
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
36
39
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
37
40
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
38
41
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -41,6 +44,15 @@ var UBUNTU_2604_X64 = [
|
|
|
41
44
|
{ requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
|
|
42
45
|
];
|
|
43
46
|
var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
47
|
+
var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
|
|
48
|
+
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
49
|
+
"data-root": "/var/lib/docker",
|
|
50
|
+
"storage-driver": "overlay2",
|
|
51
|
+
"live-restore": true,
|
|
52
|
+
"log-driver": "local",
|
|
53
|
+
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
54
|
+
}, null, 2)}
|
|
55
|
+
`;
|
|
44
56
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
45
57
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
46
58
|
let child;
|
|
@@ -83,6 +95,15 @@ async function executeSoftwareOperation(operation) {
|
|
|
83
95
|
if (operation.kind === "check") {
|
|
84
96
|
if (software === "bun")
|
|
85
97
|
return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
|
|
98
|
+
if (software === "docker") {
|
|
99
|
+
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
100
|
+
if (!successful(binary, /Docker version/))
|
|
101
|
+
return { ...binary, exitCode: 1 };
|
|
102
|
+
if (!existsSync(DOCKER_DAEMON_PATH) || readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
103
|
+
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
104
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
105
|
+
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
106
|
+
}
|
|
86
107
|
if (software === "nginx") {
|
|
87
108
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
88
109
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -115,11 +136,22 @@ async function executeSoftwareOperation(operation) {
|
|
|
115
136
|
return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
|
|
116
137
|
}
|
|
117
138
|
}
|
|
118
|
-
if (software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
119
|
-
const
|
|
120
|
-
|
|
139
|
+
if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
140
|
+
const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
|
|
141
|
+
const installed = await aptInstall(packageName);
|
|
142
|
+
if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
|
|
121
143
|
return installed;
|
|
122
|
-
|
|
144
|
+
if (software === "docker") {
|
|
145
|
+
mkdirSync("/etc/docker", { recursive: true, mode: 493 });
|
|
146
|
+
if (existsSync(DOCKER_DAEMON_PATH) && readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
147
|
+
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
148
|
+
}
|
|
149
|
+
if (!existsSync(DOCKER_DAEMON_PATH))
|
|
150
|
+
writeFileSync(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
151
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
152
|
+
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
153
|
+
}
|
|
154
|
+
return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
|
|
123
155
|
}
|
|
124
156
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
125
157
|
try {
|
|
@@ -204,7 +236,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
204
236
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
205
237
|
throw new Error("software requirement contains an unknown field");
|
|
206
238
|
}
|
|
207
|
-
if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
239
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
208
240
|
throw new Error("software requirement coordinate is invalid");
|
|
209
241
|
}
|
|
210
242
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -677,13 +709,13 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
677
709
|
}
|
|
678
710
|
|
|
679
711
|
// src/software-helper.ts
|
|
680
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
712
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
|
|
681
713
|
import { connect, createServer } from "node:net";
|
|
682
|
-
import { dirname as
|
|
714
|
+
import { dirname as dirname3 } from "node:path";
|
|
683
715
|
|
|
684
716
|
// src/service-supervisor.ts
|
|
685
717
|
import { createHash as createHash2 } from "node:crypto";
|
|
686
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
718
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
687
719
|
import { dirname, join as join2, resolve, sep } from "node:path";
|
|
688
720
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
689
721
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
@@ -700,8 +732,8 @@ var defaultHost = {
|
|
|
700
732
|
renameSync2(next, path2);
|
|
701
733
|
},
|
|
702
734
|
read: (path2) => readFileSync2(path2, "utf8"),
|
|
703
|
-
exists:
|
|
704
|
-
list: (path2) =>
|
|
735
|
+
exists: existsSync2,
|
|
736
|
+
list: (path2) => existsSync2(path2) ? readdirSync(path2) : [],
|
|
705
737
|
realpath: realpathSync,
|
|
706
738
|
mkdir: (path2, mode) => mkdirSync2(path2, { recursive: true, mode }),
|
|
707
739
|
remove: (path2) => rmSync2(path2, { force: true }),
|
|
@@ -898,6 +930,297 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
898
930
|
return state;
|
|
899
931
|
}
|
|
900
932
|
|
|
933
|
+
// src/container-supervisor.ts
|
|
934
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
935
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
936
|
+
import { dirname as dirname2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
937
|
+
var defaultHost2 = {
|
|
938
|
+
realpath: realpathSync2,
|
|
939
|
+
exists: existsSync3,
|
|
940
|
+
read: (path2) => readFileSync3(path2, "utf8"),
|
|
941
|
+
write(path2, content, mode) {
|
|
942
|
+
mkdirSync3(dirname2(path2), { recursive: true, mode: 493 });
|
|
943
|
+
const next = `${path2}.next`;
|
|
944
|
+
writeFileSync3(next, content, { mode });
|
|
945
|
+
renameSync3(next, path2);
|
|
946
|
+
},
|
|
947
|
+
remove: (path2) => rmSync3(path2, { force: true }),
|
|
948
|
+
list: (path2) => existsSync3(path2) ? readdirSync2(path2) : [],
|
|
949
|
+
mkdir: (path2, mode) => mkdirSync3(path2, { recursive: true, mode }),
|
|
950
|
+
async exec(argv2) {
|
|
951
|
+
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
952
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
953
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
954
|
+
},
|
|
955
|
+
async health(port, path2, timeoutMs, method, expectedStatus) {
|
|
956
|
+
try {
|
|
957
|
+
const response = await fetch(`http://127.0.0.1:${port}${path2}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
|
|
958
|
+
return expectedStatus.includes(response.status);
|
|
959
|
+
} catch {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
964
|
+
now: Date.now
|
|
965
|
+
};
|
|
966
|
+
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
967
|
+
var idFor2 = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 24);
|
|
968
|
+
var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
969
|
+
var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
|
|
970
|
+
function validateRequest(request, host) {
|
|
971
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision) || !/^[a-z][A-Za-z0-9-]{0,62}$/.test(request.component))
|
|
972
|
+
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
973
|
+
const root = host.realpath(resolve2(request.root));
|
|
974
|
+
const release = host.realpath(resolve2(request.release));
|
|
975
|
+
if (!within2(root, release))
|
|
976
|
+
throw new Error("CONTAINER_RELEASE_OUTSIDE_ROOT");
|
|
977
|
+
const application = request.application;
|
|
978
|
+
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
979
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
980
|
+
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
981
|
+
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
982
|
+
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
983
|
+
throw new Error("CONTAINER_SECURITY_INVALID");
|
|
984
|
+
if (!application.network?.container || !["bridge", "host"].includes(application.network.container.mode))
|
|
985
|
+
throw new Error("CONTAINER_NETWORK_INVALID");
|
|
986
|
+
if (!application.network.ingress?.stablePort)
|
|
987
|
+
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
988
|
+
return { ...request, root, release };
|
|
989
|
+
}
|
|
990
|
+
async function checked2(host, argv2, code) {
|
|
991
|
+
const result = await host.exec(argv2);
|
|
992
|
+
if (result.exitCode !== 0)
|
|
993
|
+
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
994
|
+
return result.output.trim();
|
|
995
|
+
}
|
|
996
|
+
async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
997
|
+
const request = validateRequest(requestValue, host);
|
|
998
|
+
const runtime = request.application.runtime;
|
|
999
|
+
if (runtime.kind !== "container")
|
|
1000
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1001
|
+
const source = runtime.image.source;
|
|
1002
|
+
if (source.kind === "registry") {
|
|
1003
|
+
if (!source.reference.includes("@sha256:"))
|
|
1004
|
+
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
1005
|
+
await checked2(host, ["/usr/bin/docker", "pull", "--quiet", source.reference], "CONTAINER_PULL_FAILED");
|
|
1006
|
+
const digest2 = await checked2(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", source.reference], "CONTAINER_INSPECT_FAILED");
|
|
1007
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
1008
|
+
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1009
|
+
return { image: source.reference, digest: digest2 };
|
|
1010
|
+
}
|
|
1011
|
+
const context = host.realpath(resolve2(request.release, source.context));
|
|
1012
|
+
const dockerfile = host.realpath(resolve2(request.release, source.dockerfile));
|
|
1013
|
+
if (!within2(request.release, context) || !within2(context, dockerfile))
|
|
1014
|
+
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
1015
|
+
const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
|
|
1016
|
+
await checked2(host, [
|
|
1017
|
+
"/usr/bin/docker",
|
|
1018
|
+
"build",
|
|
1019
|
+
"--pull",
|
|
1020
|
+
...request.noCache ? ["--no-cache"] : [],
|
|
1021
|
+
"--file",
|
|
1022
|
+
dockerfile,
|
|
1023
|
+
"--tag",
|
|
1024
|
+
tag,
|
|
1025
|
+
"--label",
|
|
1026
|
+
`net.forgezero.revision=${request.revision}`,
|
|
1027
|
+
"--label",
|
|
1028
|
+
`net.forgezero.component=${request.component}`,
|
|
1029
|
+
context
|
|
1030
|
+
], "CONTAINER_BUILD_FAILED");
|
|
1031
|
+
const digest = await checked2(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
|
|
1032
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
1033
|
+
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1034
|
+
return { image: tag, digest };
|
|
1035
|
+
}
|
|
1036
|
+
function readState2(host, id) {
|
|
1037
|
+
if (!host.exists(statePath2(id)))
|
|
1038
|
+
return null;
|
|
1039
|
+
try {
|
|
1040
|
+
const value = JSON.parse(host.read(statePath2(id)));
|
|
1041
|
+
return value.format === 1 ? value : null;
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function usedPorts(host, exceptId) {
|
|
1047
|
+
const ports = new Set;
|
|
1048
|
+
for (const name of host.list(SERVICE_STATE_DIRECTORY).filter((entry) => /^[a-f0-9]{24}\.json$/.test(entry))) {
|
|
1049
|
+
try {
|
|
1050
|
+
const state = JSON.parse(host.read(`${SERVICE_STATE_DIRECTORY}/${name}`));
|
|
1051
|
+
if (state.id !== exceptId) {
|
|
1052
|
+
ports.add(state.publicPort);
|
|
1053
|
+
state.applicationPorts.forEach((port) => ports.add(port));
|
|
1054
|
+
}
|
|
1055
|
+
} catch {}
|
|
1056
|
+
}
|
|
1057
|
+
return ports;
|
|
1058
|
+
}
|
|
1059
|
+
function allocatePorts(host, id, count, stablePort, previous) {
|
|
1060
|
+
const used = usedPorts(host, id);
|
|
1061
|
+
if (used.has(stablePort))
|
|
1062
|
+
throw new Error("CONTAINER_PUBLIC_PORT_CONFLICT");
|
|
1063
|
+
if (previous?.applicationPorts.length === count && previous.applicationPorts.every((port) => !used.has(port)))
|
|
1064
|
+
return [...previous.applicationPorts];
|
|
1065
|
+
const from = 20000;
|
|
1066
|
+
const to = 49999;
|
|
1067
|
+
const width = to - from + 1;
|
|
1068
|
+
const start = Number.parseInt(id.slice(0, 8), 16) % width;
|
|
1069
|
+
for (let offset = 0;offset < width; offset += 1) {
|
|
1070
|
+
const first = from + (start + offset) % width;
|
|
1071
|
+
const values = Array.from({ length: count }, (_, index) => first + index);
|
|
1072
|
+
if (values.at(-1) <= to && values.every((port) => port !== stablePort && !used.has(port)))
|
|
1073
|
+
return values;
|
|
1074
|
+
}
|
|
1075
|
+
throw new Error("CONTAINER_PORTS_EXHAUSTED");
|
|
1076
|
+
}
|
|
1077
|
+
function nginx2(request, id, port) {
|
|
1078
|
+
const service = request.application.service;
|
|
1079
|
+
const stablePort = request.application.network.ingress.stablePort;
|
|
1080
|
+
return `# ForgeZero container ${id}
|
|
1081
|
+
${service.maximumConnections ? `limit_conn_zone $server_name zone=fz_${id}:64k;
|
|
1082
|
+
` : ""}server {
|
|
1083
|
+
listen 127.0.0.1:${stablePort};
|
|
1084
|
+
server_name _;
|
|
1085
|
+
${service.maximumConnections ? ` limit_conn fz_${id} ${service.maximumConnections};
|
|
1086
|
+
limit_conn_status 503;
|
|
1087
|
+
` : ""} location / {
|
|
1088
|
+
proxy_pass http://127.0.0.1:${port};
|
|
1089
|
+
proxy_http_version 1.1;
|
|
1090
|
+
${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
1091
|
+
proxy_set_header Connection "upgrade";
|
|
1092
|
+
` : ""} proxy_set_header Host $host;
|
|
1093
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1094
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
`;
|
|
1098
|
+
}
|
|
1099
|
+
function runArgv(request, id, slot, port, networkName) {
|
|
1100
|
+
const { application } = request;
|
|
1101
|
+
const resources = application.resources;
|
|
1102
|
+
const runtime = application.runtime;
|
|
1103
|
+
if (runtime.kind !== "container")
|
|
1104
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1105
|
+
const memory = resources.memory;
|
|
1106
|
+
const network = application.network.container;
|
|
1107
|
+
const argv2 = [
|
|
1108
|
+
"/usr/bin/docker",
|
|
1109
|
+
"run",
|
|
1110
|
+
"--detach",
|
|
1111
|
+
"--name",
|
|
1112
|
+
nameFor(id, slot),
|
|
1113
|
+
"--restart",
|
|
1114
|
+
"unless-stopped",
|
|
1115
|
+
"--label",
|
|
1116
|
+
`net.forgezero.key=${request.key}`,
|
|
1117
|
+
"--label",
|
|
1118
|
+
`net.forgezero.revision=${request.revision}`,
|
|
1119
|
+
"--cpus",
|
|
1120
|
+
String(resources.cpu.limit),
|
|
1121
|
+
"--cpu-shares",
|
|
1122
|
+
String(resources.cpu.weight ?? 100),
|
|
1123
|
+
"--memory",
|
|
1124
|
+
`${memory.limitMiB}m`,
|
|
1125
|
+
...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
|
|
1126
|
+
...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
|
|
1127
|
+
"--pids-limit",
|
|
1128
|
+
String(resources.pids.limit),
|
|
1129
|
+
"--security-opt",
|
|
1130
|
+
"no-new-privileges=true",
|
|
1131
|
+
"--cap-drop",
|
|
1132
|
+
"ALL",
|
|
1133
|
+
...runtime.security.root === "read-only" ? ["--read-only"] : [],
|
|
1134
|
+
...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
|
|
1135
|
+
];
|
|
1136
|
+
for (const mount of application.storage ?? []) {
|
|
1137
|
+
if (mount.class === "ephemeral")
|
|
1138
|
+
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
1139
|
+
else
|
|
1140
|
+
argv2.push("--volume", `forgezero-${id}-${mount.name}:${mount.path}`);
|
|
1141
|
+
}
|
|
1142
|
+
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
1143
|
+
return argv2;
|
|
1144
|
+
}
|
|
1145
|
+
async function activateContainer(requestValue, host = defaultHost2) {
|
|
1146
|
+
const request = validateRequest(requestValue, host);
|
|
1147
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(request.image) && !/^forgezero\/[a-f0-9]{24}:[a-f0-9]{40}$/.test(request.image) && !request.image.includes("@sha256:"))
|
|
1148
|
+
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
1149
|
+
const id = idFor2(request.key);
|
|
1150
|
+
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1151
|
+
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
1152
|
+
const previous = readState2(host, id);
|
|
1153
|
+
const slots = request.application.rollout.strategy === "blue-green" ? 2 : 1;
|
|
1154
|
+
if (!["direct", "blue-green"].includes(request.application.rollout.strategy))
|
|
1155
|
+
throw new Error("CONTAINER_ROLLOUT_REQUIRES_CONTROL_PLANE");
|
|
1156
|
+
const containerNetwork = request.application.network.container;
|
|
1157
|
+
let networkName = "host";
|
|
1158
|
+
if (containerNetwork.mode === "bridge") {
|
|
1159
|
+
networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
|
|
1160
|
+
if (networkName !== "bridge") {
|
|
1161
|
+
const inspected = await host.exec(["/usr/bin/docker", "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName]);
|
|
1162
|
+
if (inspected.exitCode !== 0) {
|
|
1163
|
+
await checked2(host, ["/usr/bin/docker", "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName], "CONTAINER_NETWORK_CREATE_FAILED");
|
|
1164
|
+
} else if (inspected.output.trim() !== id)
|
|
1165
|
+
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const ports = allocatePorts(host, id, slots, request.application.network.ingress.stablePort, previous);
|
|
1169
|
+
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
1170
|
+
const port = ports[nextSlot];
|
|
1171
|
+
const name = nameFor(id, nextSlot);
|
|
1172
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
1173
|
+
await checked2(host, runArgv(request, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
1174
|
+
let healthy = false;
|
|
1175
|
+
const health = request.application.service.health;
|
|
1176
|
+
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
1177
|
+
if (await host.health(port, health.path, health.timeoutMs, health.method, health.expectedStatus)) {
|
|
1178
|
+
healthy = true;
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
await host.sleep(health.intervalMs ?? 1000);
|
|
1182
|
+
}
|
|
1183
|
+
if (!healthy) {
|
|
1184
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
1185
|
+
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
1186
|
+
}
|
|
1187
|
+
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
|
|
1188
|
+
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1189
|
+
host.write(nginxPath, nginx2(request, id, port), 420);
|
|
1190
|
+
try {
|
|
1191
|
+
await checked2(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
1192
|
+
await checked2(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
1193
|
+
} catch (cause) {
|
|
1194
|
+
if (previousNginx === null)
|
|
1195
|
+
host.remove(nginxPath);
|
|
1196
|
+
else
|
|
1197
|
+
host.write(nginxPath, previousNginx, 420);
|
|
1198
|
+
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
1199
|
+
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
1200
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
1201
|
+
throw cause;
|
|
1202
|
+
}
|
|
1203
|
+
const state = {
|
|
1204
|
+
format: 1,
|
|
1205
|
+
id,
|
|
1206
|
+
key: request.key,
|
|
1207
|
+
revision: request.revision,
|
|
1208
|
+
strategy: request.application.rollout.strategy,
|
|
1209
|
+
publicPort: request.application.network.ingress.stablePort,
|
|
1210
|
+
applicationPorts: ports,
|
|
1211
|
+
activeSlot: nextSlot,
|
|
1212
|
+
healthPath: health.path,
|
|
1213
|
+
updatedAtTs: host.now()
|
|
1214
|
+
};
|
|
1215
|
+
host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
|
|
1216
|
+
`, 384);
|
|
1217
|
+
if (previous && previous.activeSlot !== nextSlot) {
|
|
1218
|
+
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
1219
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", nameFor(id, previous.activeSlot)]);
|
|
1220
|
+
}
|
|
1221
|
+
return state;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
901
1224
|
// src/software-helper.ts
|
|
902
1225
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
903
1226
|
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
@@ -906,11 +1229,13 @@ var MAX_REQUEST_BYTES = 128 * 1024;
|
|
|
906
1229
|
var MAX_PENDING_REQUESTS = 128;
|
|
907
1230
|
function startSoftwareHelper(options = {}) {
|
|
908
1231
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
909
|
-
if (
|
|
1232
|
+
if (existsSync4(socketPath))
|
|
910
1233
|
unlinkSync2(socketPath);
|
|
911
|
-
|
|
1234
|
+
mkdirSync4(dirname3(socketPath), { recursive: true, mode: 488 });
|
|
912
1235
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
913
1236
|
const activate = options.activate ?? activateSupervisedService;
|
|
1237
|
+
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
1238
|
+
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
914
1239
|
let tail = Promise.resolve();
|
|
915
1240
|
let pending = 0;
|
|
916
1241
|
const server = createServer((socket) => {
|
|
@@ -943,10 +1268,29 @@ function startSoftwareHelper(options = {}) {
|
|
|
943
1268
|
return;
|
|
944
1269
|
}
|
|
945
1270
|
if (request.op === "activate-service" && request.request) {
|
|
946
|
-
|
|
1271
|
+
const serviceRequest = request.request;
|
|
1272
|
+
if (!options.allowedRoot || serviceRequest.root !== options.allowedRoot) {
|
|
947
1273
|
throw new Error("service deployment root is not owned by this helper");
|
|
948
1274
|
}
|
|
949
|
-
const state = await activate(
|
|
1275
|
+
const state = await activate(serviceRequest);
|
|
1276
|
+
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
1277
|
+
`);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (request.op === "build-container" && request.request) {
|
|
1281
|
+
const containerRequest = request.request;
|
|
1282
|
+
if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
|
|
1283
|
+
throw new Error("container deployment root is not owned by this helper");
|
|
1284
|
+
const result = await buildContainer(containerRequest);
|
|
1285
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
1286
|
+
`);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (request.op === "activate-container" && request.request) {
|
|
1290
|
+
const containerRequest = request.request;
|
|
1291
|
+
if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
|
|
1292
|
+
throw new Error("container deployment root is not owned by this helper");
|
|
1293
|
+
const state = await activateTypedContainer(containerRequest);
|
|
950
1294
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
951
1295
|
`);
|
|
952
1296
|
return;
|
|
@@ -970,8 +1314,43 @@ function startSoftwareHelper(options = {}) {
|
|
|
970
1314
|
server.listen(socketPath, () => chmodSync2(socketPath, 432));
|
|
971
1315
|
return server;
|
|
972
1316
|
}
|
|
1317
|
+
function requestContainer(op, request, socketPath, timeoutMs) {
|
|
1318
|
+
return new Promise((resolve3, reject) => {
|
|
1319
|
+
const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
|
|
1320
|
+
`));
|
|
1321
|
+
let buffer = "";
|
|
1322
|
+
socket.setTimeout(timeoutMs, () => {
|
|
1323
|
+
socket.destroy();
|
|
1324
|
+
reject(new Error("container helper timed out"));
|
|
1325
|
+
});
|
|
1326
|
+
socket.on("data", (chunk) => {
|
|
1327
|
+
buffer += chunk.toString("utf8");
|
|
1328
|
+
const newline = buffer.indexOf(`
|
|
1329
|
+
`);
|
|
1330
|
+
if (newline < 0)
|
|
1331
|
+
return;
|
|
1332
|
+
socket.end();
|
|
1333
|
+
try {
|
|
1334
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
1335
|
+
const result = response.result ?? response.state;
|
|
1336
|
+
if (!response.ok || !result)
|
|
1337
|
+
throw new Error(response.error?.message ?? "container helper refused request");
|
|
1338
|
+
resolve3(result);
|
|
1339
|
+
} catch (cause) {
|
|
1340
|
+
reject(cause);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
socket.on("error", reject);
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 30 * 60000) {
|
|
1347
|
+
return requestContainer("build-container", request, socketPath, timeoutMs);
|
|
1348
|
+
}
|
|
1349
|
+
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1350
|
+
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
1351
|
+
}
|
|
973
1352
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
974
|
-
return new Promise((
|
|
1353
|
+
return new Promise((resolve3, reject) => {
|
|
975
1354
|
const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
976
1355
|
`));
|
|
977
1356
|
let buffer = "";
|
|
@@ -990,7 +1369,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
990
1369
|
const response = JSON.parse(buffer.slice(0, newline));
|
|
991
1370
|
if (!response.ok || !response.state)
|
|
992
1371
|
throw new Error(response.error?.message ?? "service helper refused activation");
|
|
993
|
-
|
|
1372
|
+
resolve3(response.state);
|
|
994
1373
|
} catch (cause) {
|
|
995
1374
|
reject(cause);
|
|
996
1375
|
}
|
|
@@ -1000,7 +1379,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
1000
1379
|
}
|
|
1001
1380
|
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1002
1381
|
validateSoftwareRequirements(requirements);
|
|
1003
|
-
return new Promise((
|
|
1382
|
+
return new Promise((resolve3, reject) => {
|
|
1004
1383
|
const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
1005
1384
|
`));
|
|
1006
1385
|
let buffer = "";
|
|
@@ -1019,7 +1398,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
1019
1398
|
const response = JSON.parse(buffer.slice(0, newline));
|
|
1020
1399
|
if (!response.ok || !response.results)
|
|
1021
1400
|
throw new Error(response.error?.message ?? "software helper refused the request");
|
|
1022
|
-
|
|
1401
|
+
resolve3(response.results);
|
|
1023
1402
|
} catch (cause) {
|
|
1024
1403
|
reject(cause);
|
|
1025
1404
|
}
|
|
@@ -1031,6 +1410,8 @@ export {
|
|
|
1031
1410
|
startSoftwareHelper,
|
|
1032
1411
|
requestSoftware,
|
|
1033
1412
|
requestServiceActivation,
|
|
1413
|
+
requestContainerBuild,
|
|
1414
|
+
requestContainerActivation,
|
|
1034
1415
|
SOFTWARE_HELPER_UNIT_PATH,
|
|
1035
1416
|
SOFTWARE_HELPER_GROUP,
|
|
1036
1417
|
DEFAULT_SOFTWARE_HELPER_SOCKET
|
package/dist/software.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type CatalogStatus = 'testing' | 'active' | 'retired';
|
|
2
2
|
export type DeploymentChannel = 'development' | 'production';
|
|
3
|
-
export type SoftwareId = 'bun' | 'nginx' | 'arangodb' | 'cloudflared' | 'cloudflare-warp' | 'ufw' | 'openssh-client';
|
|
3
|
+
export type SoftwareId = 'bun' | 'docker' | 'nginx' | 'arangodb' | 'cloudflared' | 'cloudflare-warp' | 'ufw' | 'openssh-client';
|
|
4
4
|
/** Repository input is a catalogue coordinate, never a root command. */
|
|
5
5
|
export interface SoftwareRequirement {
|
|
6
6
|
id: SoftwareId;
|
package/dist/software.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
accessSync,
|
|
5
5
|
chmodSync,
|
|
6
6
|
copyFileSync,
|
|
7
|
+
existsSync,
|
|
7
8
|
mkdtempSync,
|
|
8
9
|
mkdirSync,
|
|
9
10
|
readFileSync,
|
|
@@ -24,6 +25,7 @@ var OS_CATALOG = [
|
|
|
24
25
|
];
|
|
25
26
|
var SOFTWARE_CATALOG = [
|
|
26
27
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
28
|
+
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
27
29
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
28
30
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
29
31
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -33,6 +35,7 @@ var SOFTWARE_CATALOG = [
|
|
|
33
35
|
];
|
|
34
36
|
var UBUNTU_2604_X64 = [
|
|
35
37
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
38
|
+
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
36
39
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
37
40
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
38
41
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -41,6 +44,15 @@ var UBUNTU_2604_X64 = [
|
|
|
41
44
|
{ requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
|
|
42
45
|
];
|
|
43
46
|
var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
47
|
+
var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
|
|
48
|
+
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
49
|
+
"data-root": "/var/lib/docker",
|
|
50
|
+
"storage-driver": "overlay2",
|
|
51
|
+
"live-restore": true,
|
|
52
|
+
"log-driver": "local",
|
|
53
|
+
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
54
|
+
}, null, 2)}
|
|
55
|
+
`;
|
|
44
56
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
45
57
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
46
58
|
let child;
|
|
@@ -83,6 +95,15 @@ async function executeSoftwareOperation(operation) {
|
|
|
83
95
|
if (operation.kind === "check") {
|
|
84
96
|
if (software === "bun")
|
|
85
97
|
return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
|
|
98
|
+
if (software === "docker") {
|
|
99
|
+
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
100
|
+
if (!successful(binary, /Docker version/))
|
|
101
|
+
return { ...binary, exitCode: 1 };
|
|
102
|
+
if (!existsSync(DOCKER_DAEMON_PATH) || readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
103
|
+
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
104
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
105
|
+
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
106
|
+
}
|
|
86
107
|
if (software === "nginx") {
|
|
87
108
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
88
109
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -115,11 +136,22 @@ async function executeSoftwareOperation(operation) {
|
|
|
115
136
|
return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
|
|
116
137
|
}
|
|
117
138
|
}
|
|
118
|
-
if (software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
119
|
-
const
|
|
120
|
-
|
|
139
|
+
if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
140
|
+
const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
|
|
141
|
+
const installed = await aptInstall(packageName);
|
|
142
|
+
if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
|
|
121
143
|
return installed;
|
|
122
|
-
|
|
144
|
+
if (software === "docker") {
|
|
145
|
+
mkdirSync("/etc/docker", { recursive: true, mode: 493 });
|
|
146
|
+
if (existsSync(DOCKER_DAEMON_PATH) && readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
147
|
+
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
148
|
+
}
|
|
149
|
+
if (!existsSync(DOCKER_DAEMON_PATH))
|
|
150
|
+
writeFileSync(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
151
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
152
|
+
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
153
|
+
}
|
|
154
|
+
return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
|
|
123
155
|
}
|
|
124
156
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
125
157
|
try {
|
|
@@ -204,7 +236,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
204
236
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
205
237
|
throw new Error("software requirement contains an unknown field");
|
|
206
238
|
}
|
|
207
|
-
if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
239
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
208
240
|
throw new Error("software requirement coordinate is invalid");
|
|
209
241
|
}
|
|
210
242
|
const requirement = { id: row.id, version: row.version };
|
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.56";
|