@forgezero/agent 0.1.29 → 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.
@@ -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
+ };
@@ -83,7 +83,7 @@ export interface UnitOptions {
83
83
  controlSocketPath?: string;
84
84
  repository?: string;
85
85
  branch?: string;
86
- role?: string;
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 === "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
- if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
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.29";
572
+ var VERSION2 = "0.1.30";
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.role ? `FZ_DEPLOY_ROLE=${options.role}` : null,
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,
@@ -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
- if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
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 = [];
@@ -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: 'bun' | 'nginx' | 'arangodb' | 'cloudflared' | 'ufw';
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): SoftwareRequirement[];
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 === "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
- if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
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.29";
2
+ export declare const VERSION = "0.1.30";
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.29",
4
+ "version": "0.1.30",
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/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": {
@@ -123,6 +123,10 @@
123
123
  "./ubuntu": {
124
124
  "types": "./dist/ubuntu.d.ts",
125
125
  "default": "./dist/ubuntu.js"
126
+ },
127
+ "./project-context": {
128
+ "types": "./dist/project-context.d.ts",
129
+ "default": "./dist/project-context.js"
126
130
  }
127
131
  }
128
132
  }