@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.
@@ -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
+ };
@@ -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. */
@@ -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
- 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.28";
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.role ? `FZ_DEPLOY_ROLE=${options.role}` : null,
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 start",
1222
- command: `systemctl enable --now ${[
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 vault socket exists", command: `test -S ${options.socketPath}` },
1238
- { label: "prove the Agent update helper exists", command: `test -S ${DEFAULT_AGENT_UPDATE_SOCKET}` },
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: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
1306
+ command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
1242
1307
  }, {
1243
1308
  label: "prove the software strategy helper socket exists",
1244
- command: `test -S ${DEFAULT_SOFTWARE_HELPER_SOCKET}`
1309
+ command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
1245
1310
  }] : [],
1246
1311
  ...lifecycleEnabled ? [{
1247
1312
  label: "prove the lifecycle helper socket exists",
1248
- command: `test -S ${lifecycleHelperSocketPath}`
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: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
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
  };
@@ -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;