@forgezero/agent 0.1.28 → 0.1.30
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 +24 -4
- package/dist/agent-heartbeat.js +1 -1
- package/dist/cli/agent-install.d.ts +1 -1
- package/dist/definition.d.ts +14 -8
- package/dist/definition.js +58 -23
- package/dist/deployment-pull.d.ts +3 -3
- package/dist/deployment.d.ts +5 -5
- package/dist/fz-agent.js +80 -48
- package/dist/fz.js +393 -31
- package/dist/index.d.ts +2 -2
- package/dist/metal-helper-socket.js +2 -3
- package/dist/metal-provision.js +2 -3
- package/dist/project-context.d.ts +37 -0
- package/dist/project-context.js +266 -0
- package/dist/provision.d.ts +9 -1
- package/dist/provision.js +85 -17
- package/dist/software-helper.js +17 -2
- package/dist/software.d.ts +24 -2
- package/dist/software.js +20 -3
- package/dist/version.d.ts +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const PROJECT_CONTEXT_VERSION: 1;
|
|
2
|
+
export interface ProjectTruthSource {
|
|
3
|
+
area: string;
|
|
4
|
+
path: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectContextManifest {
|
|
8
|
+
schemaVersion: typeof PROJECT_CONTEXT_VERSION;
|
|
9
|
+
name: string;
|
|
10
|
+
purpose: string;
|
|
11
|
+
truth: readonly ProjectTruthSource[];
|
|
12
|
+
readFirst: readonly string[];
|
|
13
|
+
verify: readonly string[];
|
|
14
|
+
rules: readonly string[];
|
|
15
|
+
/** Files or directories that are generated, historical, or otherwise not authoritative. */
|
|
16
|
+
nonAuthoritative: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ContextFile {
|
|
19
|
+
path: string;
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ContextCheck {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
problems: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class ProjectContextError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
export declare function parseProjectContext(value: unknown): ProjectContextManifest;
|
|
30
|
+
export declare function defaultProjectContext(root?: string): ProjectContextManifest;
|
|
31
|
+
export declare function renderProjectContext(manifest: ProjectContextManifest): string;
|
|
32
|
+
export declare function projectContextFiles(manifestInput: ProjectContextManifest): ContextFile[];
|
|
33
|
+
export declare function initializeProjectContext(rootInput: string, manifestInput?: ProjectContextManifest, options?: {
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}): ContextFile[];
|
|
36
|
+
export declare function syncProjectContext(rootInput: string): ContextFile[];
|
|
37
|
+
export declare function checkProjectContext(rootInput: string): ContextCheck;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/project-context.ts
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
|
+
var PROJECT_CONTEXT_VERSION = 1;
|
|
6
|
+
var GENERATED = "<!-- Generated by @forgezero/agent project context. Edit .forgezero/project.json, then run `fz project sync`. -->";
|
|
7
|
+
|
|
8
|
+
class ProjectContextError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ProjectContextError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
var text = (value, where) => {
|
|
15
|
+
if (typeof value !== "string" || !value.trim() || /[\r\0]/.test(value)) {
|
|
16
|
+
throw new ProjectContextError(`${where} must be non-empty text.`);
|
|
17
|
+
}
|
|
18
|
+
return value.trim();
|
|
19
|
+
};
|
|
20
|
+
var relativePath = (value, where) => {
|
|
21
|
+
const path = text(value, where);
|
|
22
|
+
if (path.startsWith("/") || path.split("/").includes("..")) {
|
|
23
|
+
throw new ProjectContextError(`${where} must stay inside the repository.`);
|
|
24
|
+
}
|
|
25
|
+
return path.replace(/^\.\//, "");
|
|
26
|
+
};
|
|
27
|
+
var stringList = (value, where, paths = false) => {
|
|
28
|
+
if (!Array.isArray(value) || value.length > 128) {
|
|
29
|
+
throw new ProjectContextError(`${where} must be an array of at most 128 entries.`);
|
|
30
|
+
}
|
|
31
|
+
const items = value.map((item, index) => paths ? relativePath(item, `${where}[${index}]`) : text(item, `${where}[${index}]`));
|
|
32
|
+
if (new Set(items).size !== items.length)
|
|
33
|
+
throw new ProjectContextError(`${where} must not contain duplicates.`);
|
|
34
|
+
return items;
|
|
35
|
+
};
|
|
36
|
+
function parseProjectContext(value) {
|
|
37
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
38
|
+
throw new ProjectContextError("project context must be an object.");
|
|
39
|
+
}
|
|
40
|
+
const row = value;
|
|
41
|
+
const allowed = ["schemaVersion", "name", "purpose", "truth", "readFirst", "verify", "rules", "nonAuthoritative"];
|
|
42
|
+
const unknown = Object.keys(row).filter((key) => !allowed.includes(key));
|
|
43
|
+
if (unknown.length)
|
|
44
|
+
throw new ProjectContextError(`project context contains unknown field(s): ${unknown.join(", ")}.`);
|
|
45
|
+
if (row.schemaVersion !== PROJECT_CONTEXT_VERSION) {
|
|
46
|
+
throw new ProjectContextError(`project context schemaVersion must be ${PROJECT_CONTEXT_VERSION}.`);
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(row.truth) || row.truth.length === 0 || row.truth.length > 64) {
|
|
49
|
+
throw new ProjectContextError("project context truth must contain from 1 to 64 sources.");
|
|
50
|
+
}
|
|
51
|
+
const truth = row.truth.map((item, index) => {
|
|
52
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
53
|
+
throw new ProjectContextError(`truth[${index}] must be an object.`);
|
|
54
|
+
}
|
|
55
|
+
const source = item;
|
|
56
|
+
if (Object.keys(source).some((key) => !["area", "path", "description"].includes(key))) {
|
|
57
|
+
throw new ProjectContextError(`truth[${index}] contains an unknown field.`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
area: text(source.area, `truth[${index}].area`),
|
|
61
|
+
path: relativePath(source.path, `truth[${index}].path`),
|
|
62
|
+
description: text(source.description, `truth[${index}].description`)
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
const areas = truth.map((source) => source.area);
|
|
66
|
+
if (new Set(areas).size !== areas.length)
|
|
67
|
+
throw new ProjectContextError("project context truth areas must be unique.");
|
|
68
|
+
return {
|
|
69
|
+
schemaVersion: PROJECT_CONTEXT_VERSION,
|
|
70
|
+
name: text(row.name, "project context name"),
|
|
71
|
+
purpose: text(row.purpose, "project context purpose"),
|
|
72
|
+
truth,
|
|
73
|
+
readFirst: stringList(row.readFirst, "project context readFirst", true),
|
|
74
|
+
verify: stringList(row.verify, "project context verify"),
|
|
75
|
+
rules: stringList(row.rules, "project context rules"),
|
|
76
|
+
nonAuthoritative: stringList(row.nonAuthoritative, "project context nonAuthoritative", true)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function defaultProjectContext(root = process.cwd()) {
|
|
80
|
+
let name = root.split("/").filter(Boolean).at(-1) ?? "project";
|
|
81
|
+
let verify = ["npm test"];
|
|
82
|
+
const manifestPath = join(root, "package.json");
|
|
83
|
+
if (existsSync(manifestPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const pkg = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
86
|
+
name = pkg.name ?? name;
|
|
87
|
+
const runner = existsSync(join(root, "bun.lock")) ? "bun run" : "npm run";
|
|
88
|
+
verify = ["check", "test", "build"].filter((script) => pkg.scripts?.[script]).map((script) => `${runner} ${script}`);
|
|
89
|
+
if (verify.length === 0)
|
|
90
|
+
verify = [existsSync(join(root, "bun.lock")) ? "bun test" : "npm test"];
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
schemaVersion: PROJECT_CONTEXT_VERSION,
|
|
95
|
+
name,
|
|
96
|
+
purpose: "Describe the product outcome here; implementation details belong in the truth sources below.",
|
|
97
|
+
truth: [
|
|
98
|
+
{ area: "architecture", path: "docs/architecture.md", description: "Current system boundaries and decisions." },
|
|
99
|
+
{ area: "progress", path: "docs/progress.md", description: "Evidence-backed delivery state and next work." }
|
|
100
|
+
],
|
|
101
|
+
readFirst: [".forgezero/PROJECT.md"],
|
|
102
|
+
verify,
|
|
103
|
+
rules: [
|
|
104
|
+
"Inspect the current worktree before editing and preserve unrelated changes.",
|
|
105
|
+
"Update a truth source instead of copying architecture or progress into another document.",
|
|
106
|
+
"Never report a feature as complete without running its declared verification."
|
|
107
|
+
],
|
|
108
|
+
nonAuthoritative: ["audit/"]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function renderProjectContext(manifest) {
|
|
112
|
+
const truth = manifest.truth.map((source) => `| ${source.area} | \`${source.path}\` | ${source.description} |`).join(`
|
|
113
|
+
`);
|
|
114
|
+
return `${GENERATED}
|
|
115
|
+
# ${manifest.name} \u2014 project context
|
|
116
|
+
|
|
117
|
+
${manifest.purpose}
|
|
118
|
+
|
|
119
|
+
## Read first
|
|
120
|
+
|
|
121
|
+
${manifest.readFirst.map((path) => `- \`${path}\``).join(`
|
|
122
|
+
`) || "- No additional entry points."}
|
|
123
|
+
|
|
124
|
+
## Sources of truth
|
|
125
|
+
|
|
126
|
+
| Area | Path | Authority |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
${truth}
|
|
129
|
+
|
|
130
|
+
If two files disagree, the file named in this table wins. Fix or regenerate the
|
|
131
|
+
other file in the same change. Conversation memory, audit snapshots and generated
|
|
132
|
+
output never override repository truth.
|
|
133
|
+
|
|
134
|
+
## Project rules
|
|
135
|
+
|
|
136
|
+
${manifest.rules.map((rule) => `- ${rule}`).join(`
|
|
137
|
+
`) || "- No additional project rules."}
|
|
138
|
+
|
|
139
|
+
## Verification
|
|
140
|
+
|
|
141
|
+
${manifest.verify.map((command) => `- \`${command}\``).join(`
|
|
142
|
+
`) || "- No verification command declared."}
|
|
143
|
+
|
|
144
|
+
## Non-authoritative material
|
|
145
|
+
|
|
146
|
+
${manifest.nonAuthoritative.map((path) => `- \`${path}\``).join(`
|
|
147
|
+
`) || "- None declared."}
|
|
148
|
+
|
|
149
|
+
Tools, skills and AI vendors may change. They are execution aids, not memory.
|
|
150
|
+
Persist every accepted decision and status change in the source of truth that
|
|
151
|
+
owns it, then run \`fz project check\` before handoff.
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
var adapter = (name) => `${GENERATED}
|
|
155
|
+
# ${name} project instructions
|
|
156
|
+
|
|
157
|
+
Read \`.forgezero/PROJECT.md\` completely before acting. It is generated from
|
|
158
|
+
\`.forgezero/project.json\`, the vendor-neutral project context. Follow every
|
|
159
|
+
source of truth and verification command it names.
|
|
160
|
+
|
|
161
|
+
Do not treat this adapter, conversation memory, an audit report, generated
|
|
162
|
+
output, a tool, or a skill as architectural authority. When work changes an
|
|
163
|
+
accepted decision or delivery state, update the named Git source in the same
|
|
164
|
+
change and run \`fz project check\`.
|
|
165
|
+
`;
|
|
166
|
+
function projectContextFiles(manifestInput) {
|
|
167
|
+
const manifest = parseProjectContext(manifestInput);
|
|
168
|
+
return [
|
|
169
|
+
{ path: ".forgezero/PROJECT.md", content: renderProjectContext(manifest) },
|
|
170
|
+
{ path: "AGENTS.md", content: adapter("AI agent") },
|
|
171
|
+
{ path: "CLAUDE.md", content: adapter("Claude") },
|
|
172
|
+
{ path: "GEMINI.md", content: adapter("Gemini") },
|
|
173
|
+
{ path: ".github/copilot-instructions.md", content: adapter("GitHub Copilot") },
|
|
174
|
+
{ path: ".cursor/rules/project-context.mdc", content: `${GENERATED}
|
|
175
|
+
---
|
|
176
|
+
description: Repository source-of-truth contract
|
|
177
|
+
alwaysApply: true
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
${adapter("Cursor").replace(`${GENERATED}
|
|
181
|
+
`, "")}` }
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
var atomicWrite = (path, content) => {
|
|
185
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
186
|
+
const next = `${path}.${process.pid}.next`;
|
|
187
|
+
writeFileSync(next, content, { mode: 420 });
|
|
188
|
+
renameSync(next, path);
|
|
189
|
+
};
|
|
190
|
+
function initializeProjectContext(rootInput, manifestInput = defaultProjectContext(rootInput), options = {}) {
|
|
191
|
+
const root = resolve(rootInput);
|
|
192
|
+
const manifest = parseProjectContext(manifestInput);
|
|
193
|
+
const manifestPath = join(root, ".forgezero", "project.json");
|
|
194
|
+
const files = projectContextFiles(manifest);
|
|
195
|
+
const collisions = [manifestPath, ...files.map((file) => join(root, file.path))].filter((path) => {
|
|
196
|
+
if (!existsSync(path))
|
|
197
|
+
return false;
|
|
198
|
+
if (path === manifestPath)
|
|
199
|
+
return true;
|
|
200
|
+
return !readFileSync(path, "utf8").startsWith(GENERATED);
|
|
201
|
+
});
|
|
202
|
+
if (collisions.length && !options.force) {
|
|
203
|
+
throw new ProjectContextError(`refusing to replace existing project context: ${collisions.join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
atomicWrite(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
206
|
+
`);
|
|
207
|
+
for (const file of files)
|
|
208
|
+
atomicWrite(join(root, file.path), file.content);
|
|
209
|
+
return files;
|
|
210
|
+
}
|
|
211
|
+
function syncProjectContext(rootInput) {
|
|
212
|
+
const root = resolve(rootInput);
|
|
213
|
+
const manifestPath = join(root, ".forgezero", "project.json");
|
|
214
|
+
if (!existsSync(manifestPath))
|
|
215
|
+
throw new ProjectContextError("No .forgezero/project.json. Run `fz project init`.");
|
|
216
|
+
const manifest = parseProjectContext(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
217
|
+
const files = projectContextFiles(manifest);
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
const path = join(root, file.path);
|
|
220
|
+
if (existsSync(path) && !readFileSync(path, "utf8").startsWith(GENERATED)) {
|
|
221
|
+
throw new ProjectContextError(`refusing to replace non-generated adapter: ${file.path}`);
|
|
222
|
+
}
|
|
223
|
+
atomicWrite(path, file.content);
|
|
224
|
+
}
|
|
225
|
+
return files;
|
|
226
|
+
}
|
|
227
|
+
function checkProjectContext(rootInput) {
|
|
228
|
+
const root = resolve(rootInput);
|
|
229
|
+
const manifestPath = join(root, ".forgezero", "project.json");
|
|
230
|
+
if (!existsSync(manifestPath))
|
|
231
|
+
return { ok: false, problems: ["missing .forgezero/project.json"] };
|
|
232
|
+
let manifest;
|
|
233
|
+
try {
|
|
234
|
+
manifest = parseProjectContext(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
235
|
+
} catch (cause) {
|
|
236
|
+
return { ok: false, problems: [cause instanceof Error ? cause.message : String(cause)] };
|
|
237
|
+
}
|
|
238
|
+
const problems = [];
|
|
239
|
+
for (const source of manifest.truth) {
|
|
240
|
+
if (!existsSync(join(root, source.path)))
|
|
241
|
+
problems.push(`missing truth source: ${source.path}`);
|
|
242
|
+
}
|
|
243
|
+
for (const path of manifest.readFirst) {
|
|
244
|
+
if (!existsSync(join(root, path)))
|
|
245
|
+
problems.push(`missing read-first file: ${path}`);
|
|
246
|
+
}
|
|
247
|
+
for (const file of projectContextFiles(manifest)) {
|
|
248
|
+
const path = join(root, file.path);
|
|
249
|
+
if (!existsSync(path))
|
|
250
|
+
problems.push(`missing generated adapter: ${file.path}`);
|
|
251
|
+
else if (readFileSync(path, "utf8") !== file.content)
|
|
252
|
+
problems.push(`drifted generated adapter: ${file.path}`);
|
|
253
|
+
}
|
|
254
|
+
return { ok: problems.length === 0, problems };
|
|
255
|
+
}
|
|
256
|
+
export {
|
|
257
|
+
syncProjectContext,
|
|
258
|
+
renderProjectContext,
|
|
259
|
+
projectContextFiles,
|
|
260
|
+
parseProjectContext,
|
|
261
|
+
initializeProjectContext,
|
|
262
|
+
defaultProjectContext,
|
|
263
|
+
checkProjectContext,
|
|
264
|
+
ProjectContextError,
|
|
265
|
+
PROJECT_CONTEXT_VERSION
|
|
266
|
+
};
|
package/dist/provision.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export interface UnitOptions {
|
|
|
83
83
|
controlSocketPath?: string;
|
|
84
84
|
repository?: string;
|
|
85
85
|
branch?: string;
|
|
86
|
-
|
|
86
|
+
profile?: string;
|
|
87
87
|
deployRoot?: string;
|
|
88
88
|
publicApiUrl?: string;
|
|
89
89
|
/** Non-secret phase values, named explicitly instead of inheriting the unit environment. */
|
|
@@ -128,6 +128,7 @@ export declare const VAULT_GROUP = "forgezero-vault";
|
|
|
128
128
|
export declare const LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
129
129
|
export declare const DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
130
130
|
export declare const AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
131
|
+
export declare const AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
131
132
|
export declare const DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
132
133
|
export declare const ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
133
134
|
export declare const LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -146,6 +147,13 @@ export declare function agentUpdateHelperUnit(options: Pick<UnitOptions, 'binPat
|
|
|
146
147
|
* terminal, and it works identically on platform and tenant computes.
|
|
147
148
|
*/
|
|
148
149
|
export declare function agentSocketUnit(options: Pick<UnitOptions, 'socketPath'>): string;
|
|
150
|
+
/**
|
|
151
|
+
* Bun does not support inheriting systemd's listening file descriptor. Keep
|
|
152
|
+
* PID 1 as the owner of the stable application socket and let systemd's small,
|
|
153
|
+
* credential-free proxy bridge it to the Agent-owned backend Unix socket.
|
|
154
|
+
*/
|
|
155
|
+
export declare function agentSocketProxyUnit(options: Pick<UnitOptions, 'socketPath' | 'user'>): string;
|
|
156
|
+
export declare function agentBackendSocketPath(publicSocketPath: string): string;
|
|
149
157
|
export declare function warpConfigUnit(options: Pick<UnitOptions, 'binPath' | 'warpOrganization' | 'warpClientIdCredentialPath' | 'warpClientSecretCredentialPath'>): string;
|
|
150
158
|
export declare function warpServiceDropIn(): string;
|
|
151
159
|
export declare function lifecycleHelperUnit(options: Pick<UnitOptions, 'binPath' | 'lifecycleProfilePath' | 'lifecycleHelperSocketPath'>): string;
|
package/dist/provision.js
CHANGED
|
@@ -361,6 +361,16 @@ import { readFileSync as readFileSync2 } from "node:fs";
|
|
|
361
361
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
362
362
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
363
363
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
364
|
+
var OS_CATALOG = [
|
|
365
|
+
{ id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
|
|
366
|
+
];
|
|
367
|
+
var SOFTWARE_CATALOG = [
|
|
368
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
369
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
370
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
371
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
372
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
373
|
+
];
|
|
364
374
|
var UBUNTU_2604_X64 = [
|
|
365
375
|
{
|
|
366
376
|
requirement: { id: "bun", version: "1.3.14" },
|
|
@@ -399,7 +409,7 @@ function observeSoftwareHost(osRelease = readFileSync2("/etc/os-release", "utf8"
|
|
|
399
409
|
architecture
|
|
400
410
|
};
|
|
401
411
|
}
|
|
402
|
-
function validateSoftwareRequirements(value) {
|
|
412
|
+
function validateSoftwareRequirements(value, options = {}) {
|
|
403
413
|
if (!Array.isArray(value) || value.length > 32)
|
|
404
414
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
405
415
|
const seen = new Set;
|
|
@@ -417,13 +427,18 @@ function validateSoftwareRequirements(value) {
|
|
|
417
427
|
if (seen.has(requirement.id))
|
|
418
428
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
419
429
|
seen.add(requirement.id);
|
|
430
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
431
|
+
if (!catalog || catalog.status === "retired" || options.channel !== "development" && catalog.status !== "active") {
|
|
432
|
+
throw new Error(`software requirement is not available for ${options.channel ?? "production"}: ${requirement.id}@${requirement.version}`);
|
|
433
|
+
}
|
|
420
434
|
return requirement;
|
|
421
435
|
});
|
|
422
436
|
}
|
|
423
437
|
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
424
438
|
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
425
439
|
const observation = options.observation ?? observeSoftwareHost();
|
|
426
|
-
|
|
440
|
+
const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
|
|
441
|
+
if (!os || os.status !== "active") {
|
|
427
442
|
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
428
443
|
}
|
|
429
444
|
const results = [];
|
|
@@ -554,7 +569,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
554
569
|
}
|
|
555
570
|
|
|
556
571
|
// src/version.ts
|
|
557
|
-
var VERSION2 = "0.1.
|
|
572
|
+
var VERSION2 = "0.1.30";
|
|
558
573
|
|
|
559
574
|
// src/provision.ts
|
|
560
575
|
function atLeast(version, floor) {
|
|
@@ -603,6 +618,7 @@ var VAULT_GROUP = "forgezero-vault";
|
|
|
603
618
|
var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
604
619
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
605
620
|
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
621
|
+
var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
606
622
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
607
623
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
608
624
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -692,17 +708,55 @@ SocketGroup=${VAULT_GROUP}
|
|
|
692
708
|
SocketMode=0660
|
|
693
709
|
DirectoryMode=0750
|
|
694
710
|
RemoveOnStop=true
|
|
695
|
-
Service=forgezero-agent.service
|
|
711
|
+
Service=forgezero-agent-proxy.service
|
|
696
712
|
|
|
697
713
|
[Install]
|
|
698
714
|
WantedBy=sockets.target
|
|
699
715
|
`;
|
|
700
716
|
}
|
|
717
|
+
function agentSocketProxyUnit(options) {
|
|
718
|
+
const backend = agentBackendSocketPath(options.socketPath);
|
|
719
|
+
const user = options.user ?? "forgezero";
|
|
720
|
+
return `[Unit]
|
|
721
|
+
Description=ForgeZero application Vault socket proxy
|
|
722
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
723
|
+
Requires=forgezero-agent.service
|
|
724
|
+
After=forgezero-agent.service
|
|
725
|
+
|
|
726
|
+
[Service]
|
|
727
|
+
User=${user}
|
|
728
|
+
Group=${VAULT_GROUP}
|
|
729
|
+
ExecStart=/usr/lib/systemd/systemd-socket-proxyd ${backend}
|
|
730
|
+
NoNewPrivileges=true
|
|
731
|
+
PrivateTmp=true
|
|
732
|
+
ProtectSystem=strict
|
|
733
|
+
ProtectHome=true
|
|
734
|
+
ProtectKernelTunables=true
|
|
735
|
+
ProtectKernelModules=true
|
|
736
|
+
ProtectControlGroups=true
|
|
737
|
+
RestrictSUIDSGID=true
|
|
738
|
+
RestrictRealtime=true
|
|
739
|
+
MemoryDenyWriteExecute=true
|
|
740
|
+
LockPersonality=true
|
|
741
|
+
RestrictAddressFamilies=AF_UNIX
|
|
742
|
+
`;
|
|
743
|
+
}
|
|
744
|
+
function agentBackendSocketPath(publicSocketPath) {
|
|
745
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
746
|
+
const backend = `${socket}.backend`;
|
|
747
|
+
if (Buffer.byteLength(backend) > 100)
|
|
748
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
749
|
+
return backend;
|
|
750
|
+
}
|
|
701
751
|
var systemdPath = (value, label) => {
|
|
702
752
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
703
753
|
throw new Error(`invalid ${label} path`);
|
|
704
754
|
return value;
|
|
705
755
|
};
|
|
756
|
+
var awaitSocketCommand = (path) => {
|
|
757
|
+
const socket = systemdPath(path, "readiness socket");
|
|
758
|
+
return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
|
|
759
|
+
};
|
|
706
760
|
var validNodeHostname = (value) => !value || value.length <= 253 && value === value.toLowerCase() && value.split(".").length >= 3 && value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
|
|
707
761
|
function warpConfigUnit(options) {
|
|
708
762
|
if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
|
|
@@ -941,7 +995,7 @@ function agentUnit(options) {
|
|
|
941
995
|
}
|
|
942
996
|
}
|
|
943
997
|
const environment = [
|
|
944
|
-
`FZ_SOCKET_PATH=${options.socketPath}`,
|
|
998
|
+
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
945
999
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
946
1000
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
947
1001
|
`FZ_AGENT_MODE=${options.mode}`,
|
|
@@ -954,7 +1008,7 @@ function agentUnit(options) {
|
|
|
954
1008
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
955
1009
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
956
1010
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
957
|
-
options.
|
|
1011
|
+
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
958
1012
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
959
1013
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
960
1014
|
deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
|
|
@@ -1021,7 +1075,6 @@ Type=simple
|
|
|
1021
1075
|
User=${user}
|
|
1022
1076
|
Group=${VAULT_GROUP}
|
|
1023
1077
|
${deploymentGroup}
|
|
1024
|
-
Sockets=forgezero-agent.socket
|
|
1025
1078
|
LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
|
|
1026
1079
|
${gitCredential}${projectCredentials}${projectCredentials ? `
|
|
1027
1080
|
` : ""}${snpPrepare}ExecStart=${bin}
|
|
@@ -1109,6 +1162,7 @@ function planProvision(options) {
|
|
|
1109
1162
|
unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
|
|
1110
1163
|
auxiliaryUnits: [
|
|
1111
1164
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
1165
|
+
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
1112
1166
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
1113
1167
|
...deploymentEnabled ? [
|
|
1114
1168
|
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
|
|
@@ -1184,6 +1238,10 @@ function planProvision(options) {
|
|
|
1184
1238
|
label: "credential directory",
|
|
1185
1239
|
command: `install -d -o root -g root -m 0700 ${credentialDir}`
|
|
1186
1240
|
},
|
|
1241
|
+
{
|
|
1242
|
+
label: "Agent state directory",
|
|
1243
|
+
command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
|
|
1244
|
+
},
|
|
1187
1245
|
{
|
|
1188
1246
|
label: "encrypted node identity",
|
|
1189
1247
|
command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
|
|
@@ -1218,8 +1276,8 @@ function planProvision(options) {
|
|
|
1218
1276
|
command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
|
|
1219
1277
|
}] : [],
|
|
1220
1278
|
{
|
|
1221
|
-
label: "enable and
|
|
1222
|
-
command: `systemctl enable
|
|
1279
|
+
label: "enable and converge services",
|
|
1280
|
+
command: `systemctl enable ${[
|
|
1223
1281
|
"forgezero-agent.socket",
|
|
1224
1282
|
"forgezero-agent-update-helper.service",
|
|
1225
1283
|
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
@@ -1227,25 +1285,32 @@ function planProvision(options) {
|
|
|
1227
1285
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
1228
1286
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
1229
1287
|
"forgezero-agent.service"
|
|
1230
|
-
].join(" ")}
|
|
1288
|
+
].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
|
|
1289
|
+
"forgezero-agent-update-helper.service",
|
|
1290
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
1291
|
+
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
1292
|
+
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
1293
|
+
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
|
|
1294
|
+
].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
|
|
1231
1295
|
},
|
|
1232
1296
|
...enrolmentEnabled ? [{
|
|
1233
1297
|
label: "prove the compute binding is durable",
|
|
1234
1298
|
command: `test -s ${enrolStatePath}`
|
|
1235
1299
|
}] : [],
|
|
1236
1300
|
{ label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
|
|
1237
|
-
{ label: "prove the
|
|
1238
|
-
{ label: "prove the Agent
|
|
1301
|
+
{ label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
|
|
1302
|
+
{ label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
|
|
1303
|
+
{ label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
|
|
1239
1304
|
...deploymentEnabled ? [{
|
|
1240
1305
|
label: "prove the deployment runner socket exists",
|
|
1241
|
-
command:
|
|
1306
|
+
command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
|
|
1242
1307
|
}, {
|
|
1243
1308
|
label: "prove the software strategy helper socket exists",
|
|
1244
|
-
command:
|
|
1309
|
+
command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
|
|
1245
1310
|
}] : [],
|
|
1246
1311
|
...lifecycleEnabled ? [{
|
|
1247
1312
|
label: "prove the lifecycle helper socket exists",
|
|
1248
|
-
command:
|
|
1313
|
+
command: awaitSocketCommand(lifecycleHelperSocketPath)
|
|
1249
1314
|
}] : [],
|
|
1250
1315
|
...warpEnabled ? [{
|
|
1251
1316
|
label: "prove Cloudflare WARP is connected",
|
|
@@ -1253,7 +1318,7 @@ function planProvision(options) {
|
|
|
1253
1318
|
}] : [],
|
|
1254
1319
|
...options.repository ? [{
|
|
1255
1320
|
label: "prove the deployment control socket exists",
|
|
1256
|
-
command:
|
|
1321
|
+
command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
|
|
1257
1322
|
}] : []
|
|
1258
1323
|
]
|
|
1259
1324
|
};
|
|
@@ -1271,7 +1336,9 @@ export {
|
|
|
1271
1336
|
agentUpdateHelperUnit,
|
|
1272
1337
|
agentUnit,
|
|
1273
1338
|
agentSocketUnit,
|
|
1339
|
+
agentSocketProxyUnit,
|
|
1274
1340
|
agentEnrolmentUnit,
|
|
1341
|
+
agentBackendSocketPath,
|
|
1275
1342
|
WARP_SERVICE_DROP_IN_PATH,
|
|
1276
1343
|
WARP_CONFIG_UNIT_PATH,
|
|
1277
1344
|
VAULT_GROUP,
|
|
@@ -1285,5 +1352,6 @@ export {
|
|
|
1285
1352
|
DEPLOYMENT_RUNNER_SOCKET,
|
|
1286
1353
|
DEPLOYMENT_GROUP,
|
|
1287
1354
|
CAPABILITY_CHECKS,
|
|
1288
|
-
AGENT_SOCKET_UNIT_PATH
|
|
1355
|
+
AGENT_SOCKET_UNIT_PATH,
|
|
1356
|
+
AGENT_SOCKET_PROXY_UNIT_PATH
|
|
1289
1357
|
};
|
package/dist/software-helper.js
CHANGED
|
@@ -3,6 +3,16 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4
4
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
5
5
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
6
|
+
var OS_CATALOG = [
|
|
7
|
+
{ id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
|
|
8
|
+
];
|
|
9
|
+
var SOFTWARE_CATALOG = [
|
|
10
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
11
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
12
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
13
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
14
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
15
|
+
];
|
|
6
16
|
var UBUNTU_2604_X64 = [
|
|
7
17
|
{
|
|
8
18
|
requirement: { id: "bun", version: "1.3.14" },
|
|
@@ -41,7 +51,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
41
51
|
architecture
|
|
42
52
|
};
|
|
43
53
|
}
|
|
44
|
-
function validateSoftwareRequirements(value) {
|
|
54
|
+
function validateSoftwareRequirements(value, options = {}) {
|
|
45
55
|
if (!Array.isArray(value) || value.length > 32)
|
|
46
56
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
47
57
|
const seen = new Set;
|
|
@@ -59,13 +69,18 @@ function validateSoftwareRequirements(value) {
|
|
|
59
69
|
if (seen.has(requirement.id))
|
|
60
70
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
61
71
|
seen.add(requirement.id);
|
|
72
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
73
|
+
if (!catalog || catalog.status === "retired" || options.channel !== "development" && catalog.status !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not available for ${options.channel ?? "production"}: ${requirement.id}@${requirement.version}`);
|
|
75
|
+
}
|
|
62
76
|
return requirement;
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
79
|
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
66
80
|
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
67
81
|
const observation = options.observation ?? observeSoftwareHost();
|
|
68
|
-
|
|
82
|
+
const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
|
|
83
|
+
if (!os || os.status !== "active") {
|
|
69
84
|
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
70
85
|
}
|
|
71
86
|
const results = [];
|
package/dist/software.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
export type CatalogStatus = 'testing' | 'active' | 'retired';
|
|
2
|
+
export type DeploymentChannel = 'development' | 'production';
|
|
3
|
+
export type SoftwareId = 'bun' | 'nginx' | 'arangodb' | 'cloudflared' | 'ufw';
|
|
1
4
|
/** Repository input is a catalogue coordinate, never a root command. */
|
|
2
5
|
export interface SoftwareRequirement {
|
|
3
|
-
id:
|
|
6
|
+
id: SoftwareId;
|
|
4
7
|
version: string;
|
|
5
8
|
}
|
|
9
|
+
export interface SoftwareCatalogEntry extends SoftwareRequirement {
|
|
10
|
+
status: CatalogStatus;
|
|
11
|
+
os: 'ubuntu';
|
|
12
|
+
osVersion: '26.04';
|
|
13
|
+
architecture: 'x64';
|
|
14
|
+
/** What promotes this coordinate beyond an unreviewed candidate. */
|
|
15
|
+
evidence: 'reviewed-strategy-and-tests';
|
|
16
|
+
}
|
|
17
|
+
export interface OsCatalogEntry {
|
|
18
|
+
id: 'ubuntu';
|
|
19
|
+
version: '26.04';
|
|
20
|
+
architecture: 'x64';
|
|
21
|
+
status: CatalogStatus;
|
|
22
|
+
}
|
|
6
23
|
export interface SoftwareObservation {
|
|
7
24
|
os: {
|
|
8
25
|
id: string;
|
|
@@ -15,8 +32,13 @@ export interface SoftwareCommandResult {
|
|
|
15
32
|
output: string;
|
|
16
33
|
}
|
|
17
34
|
export type SoftwareExec = (command: string) => Promise<SoftwareCommandResult>;
|
|
35
|
+
/** Public, command-free catalog. Root strategies remain private below. */
|
|
36
|
+
export declare const OS_CATALOG: readonly OsCatalogEntry[];
|
|
37
|
+
export declare const SOFTWARE_CATALOG: readonly SoftwareCatalogEntry[];
|
|
18
38
|
export declare function observeSoftwareHost(osRelease?: string, architecture?: NodeJS.Architecture): SoftwareObservation;
|
|
19
|
-
export declare function validateSoftwareRequirements(value: unknown
|
|
39
|
+
export declare function validateSoftwareRequirements(value: unknown, options?: {
|
|
40
|
+
channel?: DeploymentChannel;
|
|
41
|
+
}): SoftwareRequirement[];
|
|
20
42
|
export declare function ensureSoftwareRequirements(requirementsInput: unknown, options: {
|
|
21
43
|
observation?: SoftwareObservation;
|
|
22
44
|
exec: SoftwareExec;
|