@forgezero/agent 0.1.29 → 0.1.31
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 +52 -4
- package/dist/agent-heartbeat.js +1 -1
- package/dist/cli/agent-install.d.ts +1 -1
- package/dist/definition.d.ts +19 -10
- package/dist/definition.js +77 -31
- package/dist/deploy-file.d.ts +40 -0
- package/dist/deploy-file.js +378 -0
- package/dist/deployment-pull.d.ts +3 -3
- package/dist/deployment.d.ts +9 -5
- package/dist/fz-agent.js +123 -58
- package/dist/fz.js +729 -19
- package/dist/index.d.ts +2 -2
- package/dist/project-context.d.ts +37 -0
- package/dist/project-context.js +266 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +19 -4
- 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 +12 -2
- package/schema/deploy-v2.json +70 -0
package/dist/index.d.ts
CHANGED
|
@@ -32,8 +32,8 @@ export type { AgentRelease, StagedAgentRelease, UpdateCommand, UpdateCommandResu
|
|
|
32
32
|
export { activateAgentRelease, probeAgentSocket, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
|
|
33
33
|
export type { AgentUpdateRequest, AgentUpdateResponse } from './agent-update-helper';
|
|
34
34
|
export { DEFAULT_SOFTWARE_HELPER_SOCKET, requestSoftware, startSoftwareHelper, SOFTWARE_HELPER_GROUP, SOFTWARE_HELPER_UNIT_PATH } from './software-helper';
|
|
35
|
-
export { ensureSoftwareRequirements, observeSoftwareHost, validateSoftwareRequirements } from './software';
|
|
36
|
-
export type { SoftwareCommandResult, SoftwareExec, SoftwareObservation, SoftwareRequirement } from './software';
|
|
35
|
+
export { ensureSoftwareRequirements, observeSoftwareHost, validateSoftwareRequirements, OS_CATALOG, SOFTWARE_CATALOG } from './software';
|
|
36
|
+
export type { CatalogStatus, DeploymentChannel, OsCatalogEntry, SoftwareCatalogEntry, SoftwareCommandResult, SoftwareExec, SoftwareId, SoftwareObservation, SoftwareRequirement } from './software';
|
|
37
37
|
export { heartbeatAgentOnce, observeAgentHost, startAgentHeartbeat } from './agent-heartbeat';
|
|
38
38
|
export type { AgentHeartbeatOptions, AgentHeartbeatResponse, AgentObservation } from './agent-heartbeat';
|
|
39
39
|
export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
|
|
@@ -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. */
|
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 !== "active") {
|
|
432
|
+
throw new Error(`software requirement is not active: ${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.31";
|
|
558
573
|
|
|
559
574
|
// src/provision.ts
|
|
560
575
|
function atLeast(version, floor) {
|
|
@@ -993,7 +1008,7 @@ function agentUnit(options) {
|
|
|
993
1008
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
994
1009
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
995
1010
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
996
|
-
options.
|
|
1011
|
+
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
997
1012
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
998
1013
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
999
1014
|
deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
|
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 !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not active: ${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;
|
package/dist/software.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 !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not active: ${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 = [];
|
|
@@ -91,5 +106,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
|
91
106
|
export {
|
|
92
107
|
validateSoftwareRequirements,
|
|
93
108
|
observeSoftwareHost,
|
|
94
|
-
ensureSoftwareRequirements
|
|
109
|
+
ensureSoftwareRequirements,
|
|
110
|
+
SOFTWARE_CATALOG,
|
|
111
|
+
OS_CATALOG
|
|
95
112
|
};
|
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.31";
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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.31",
|
|
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 && 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",
|
|
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/deploy-file.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 && bun build src/project-context.ts --root src --outdir dist --target bun --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": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
+
"schema",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
],
|
|
@@ -72,6 +73,11 @@
|
|
|
72
73
|
"types": "./dist/definition.d.ts",
|
|
73
74
|
"default": "./dist/definition.js"
|
|
74
75
|
},
|
|
76
|
+
"./deploy-file": {
|
|
77
|
+
"types": "./dist/deploy-file.d.ts",
|
|
78
|
+
"default": "./dist/deploy-file.js"
|
|
79
|
+
},
|
|
80
|
+
"./schema/deploy-v2.json": "./schema/deploy-v2.json",
|
|
75
81
|
"./ssh-listen": {
|
|
76
82
|
"types": "./dist/ssh-listen.d.ts",
|
|
77
83
|
"default": "./dist/ssh-listen.js"
|
|
@@ -123,6 +129,10 @@
|
|
|
123
129
|
"./ubuntu": {
|
|
124
130
|
"types": "./dist/ubuntu.d.ts",
|
|
125
131
|
"default": "./dist/ubuntu.js"
|
|
132
|
+
},
|
|
133
|
+
"./project-context": {
|
|
134
|
+
"types": "./dist/project-context.d.ts",
|
|
135
|
+
"default": "./dist/project-context.js"
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://www.forgezero.net/schemas/deploy-v2.json",
|
|
4
|
+
"title": "ForgeZero deployment pipeline",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "name", "profiles", "steps"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string", "const": "https://www.forgezero.net/schemas/deploy-v2.json" },
|
|
10
|
+
"version": { "const": 2 },
|
|
11
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 120 },
|
|
12
|
+
"requireAttestation": { "type": "boolean" },
|
|
13
|
+
"profiles": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"minProperties": 1,
|
|
16
|
+
"maxProperties": 32,
|
|
17
|
+
"propertyNames": { "pattern": "^[a-z][a-z0-9-]{0,62}$" },
|
|
18
|
+
"additionalProperties": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"required": ["software"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"software": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"maxItems": 32,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["id", "version"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"id": { "enum": ["bun", "nginx", "arangodb", "cloudflared", "ufw"] },
|
|
32
|
+
"version": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"steps": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": ["name", "phase", "scope", "run"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"name": { "type": "string", "minLength": 1 },
|
|
48
|
+
"phase": { "enum": ["build", "release", "migrate", "health"] },
|
|
49
|
+
"scope": { "enum": ["target", "release"] },
|
|
50
|
+
"profiles": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"minItems": 1,
|
|
53
|
+
"uniqueItems": true,
|
|
54
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$" }
|
|
55
|
+
},
|
|
56
|
+
"run": { "type": "string", "minLength": 1 },
|
|
57
|
+
"secrets": { "type": "array", "items": { "type": "string", "pattern": "^[A-Z_][A-Z0-9_]*$" }, "uniqueItems": true },
|
|
58
|
+
"always": { "type": "boolean" },
|
|
59
|
+
"when": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"minProperties": 1,
|
|
62
|
+
"propertyNames": { "pattern": "^[A-Z_][A-Z0-9_]*$" },
|
|
63
|
+
"additionalProperties": { "type": "string", "minLength": 1 }
|
|
64
|
+
},
|
|
65
|
+
"timeoutMs": { "type": "integer", "minimum": 1, "maximum": 86400000 }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|