@forgezero/agent 0.1.124 → 0.1.128

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.
@@ -62,7 +62,7 @@ function planHostMaintenance(request, runtime = localRuntime()) {
62
62
  "--pipe",
63
63
  "--collect",
64
64
  `--unit=forgezero-${request.operation}`,
65
- "--property=User=forgezero",
65
+ "--property=User=forgezero-api",
66
66
  `--property=WorkingDirectory=${workingDirectory}`,
67
67
  `--property=EnvironmentFile=${SHARED_ENV}`,
68
68
  "--property=Environment=NODE_ENV=production",
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.124";
369
+ var VERSION = "0.1.128";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -89,7 +89,7 @@ export interface OperatorPlatformFleetCoordinates {
89
89
  databaseRole: 'master' | 'joiner';
90
90
  }[];
91
91
  }
92
- export type OperatorPlatformBootstrapMode = 'apply' | 'status';
92
+ export type OperatorPlatformBootstrapMode = 'apply' | 'repair' | 'status';
93
93
  export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'genesis-cleanup' | 'rehearsal' | 'rehearsal-cleanup' | 'host-keys' | 'rehearsal-host-keys' | 'status';
94
94
  export interface OperatorPlatformBootstrapPlan {
95
95
  kind: 'platform-remote';
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1460
1460
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1461
1461
 
1462
1462
  // src/version.ts
1463
- var VERSION = "0.1.124";
1463
+ var VERSION = "0.1.128";
1464
1464
 
1465
1465
  // src/software.ts
1466
1466
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3805,7 +3805,7 @@ function validateEnrolledComputeBootstrapSecrets(config, input) {
3805
3805
  ...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
3806
3806
  };
3807
3807
  }
3808
- function validatePlatformBootstrapSecrets(config, input) {
3808
+ function validatePlatformBootstrapSecrets(config, input, options = {}) {
3809
3809
  if (!input || typeof input !== "object" || Array.isArray(input))
3810
3810
  throw new Error("bootstrap credential input must be an object");
3811
3811
  const source = input;
@@ -3833,7 +3833,8 @@ function validatePlatformBootstrapSecrets(config, input) {
3833
3833
  throw new Error("backup credential is malformed");
3834
3834
  }
3835
3835
  const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
3836
- if (cloudflareConfigured !== Boolean(cloudflareTunnelToken && cloudflareApiToken)) {
3836
+ const cloudflareCredentialsSupplied = Boolean(cloudflareTunnelToken || cloudflareApiToken);
3837
+ if (!cloudflareConfigured && cloudflareCredentialsSupplied || cloudflareConfigured && !cloudflareCredentialsSupplied && !options.allowStoredCloudflareCredentials || Boolean(cloudflareTunnelToken) !== Boolean(cloudflareApiToken)) {
3837
3838
  throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
3838
3839
  }
3839
3840
  if (cloudflareTunnelToken)
@@ -4759,7 +4760,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4759
4760
  const platformPrivate = config.kind === "platform" ? (() => {
4760
4761
  if (!secrets)
4761
4762
  throw new Error("platform apply requires attended credentials on stdin");
4762
- const checked4 = validatePlatformBootstrapSecrets(config, secrets);
4763
+ const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
4764
+ allowStoredCloudflareCredentials: Boolean(installed?.cloudflare && config.cloudflareHandoff && !cloudflare)
4765
+ });
4763
4766
  return {
4764
4767
  root: checked4.clusterBootstrapCode,
4765
4768
  email: checked4.emailSecret,
@@ -6713,12 +6716,12 @@ function planOperatorMetalBootstrap(request, mode) {
6713
6716
  var secretSources = (config) => [
6714
6717
  ...config.cloudflareHandoff ? [["cloudflare-handoff", config.cloudflareHandoff.handoffFile]] : []
6715
6718
  ];
6716
- var attendedSecretNames = (config) => [
6719
+ var attendedSecretNames = (config, options = {}) => [
6717
6720
  "cluster-bootstrap-code",
6718
6721
  config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : "fz_smtp.password",
6719
6722
  ...config.enrolment.source === "api-token" ? ["enrol-token"] : [],
6720
6723
  ...config.runtime.environment.backup ? ["backup.s3.secretAccessKey"] : [],
6721
- ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
6724
+ ...!options.reuseBoundCloudflare && (config.cloudflareHandoff || config.runtime.environment.cloudflare) ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
6722
6725
  ];
6723
6726
  function planOperatorPlatformBootstrap(request, mode) {
6724
6727
  const config = readBootstrapConfig(request.platformConfigFile);
@@ -6742,7 +6745,10 @@ function planOperatorPlatformBootstrap(request, mode) {
6742
6745
  "run typed fz bootstrap platform --apply from the local bundle",
6743
6746
  "remove transient local and remote staging data"
6744
6747
  ],
6745
- secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
6748
+ secretInputs: mode === "apply" || mode === "repair" ? [
6749
+ ...attendedSecretNames(config, { reuseBoundCloudflare: mode === "repair" }),
6750
+ ...mode === "repair" ? [] : secretSources(config).map(([name]) => name)
6751
+ ] : []
6746
6752
  };
6747
6753
  }
6748
6754
  var fleetConfigIdentity = (config) => {
@@ -7172,10 +7178,10 @@ async function withOperatorGuestTransport(request, evidence, callback, options =
7172
7178
  rmSync6(directory, { recursive: true, force: true });
7173
7179
  }
7174
7180
  }
7175
- async function stageConfig(config, directory) {
7181
+ async function stageConfig(config, directory, options = {}) {
7176
7182
  const rewritten = structuredClone(config);
7177
7183
  const staged = [];
7178
- for (const [name, source] of secretSources(config)) {
7184
+ for (const [name, source] of options.reuseBoundCloudflare ? [] : secretSources(config)) {
7179
7185
  const bytes = ownerFile(source, SECRET_LIMIT, name);
7180
7186
  const local = join6(directory, name);
7181
7187
  writeFileSync8(local, bytes, { mode: 384, flag: "wx" });
@@ -7184,6 +7190,9 @@ async function stageConfig(config, directory) {
7184
7190
  if (name === "cloudflare-handoff")
7185
7191
  rewritten.cloudflareHandoff.handoffFile = remotePath;
7186
7192
  }
7193
+ if (options.reuseBoundCloudflare && rewritten.cloudflareHandoff) {
7194
+ rewritten.cloudflareHandoff.handoffFile = `${REMOTE_STAGE}/cloudflare-handoff`;
7195
+ }
7187
7196
  const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
7188
7197
  if (bundle.manifest.branch !== config.bootstrapBundle.branch) {
7189
7198
  throw new Error("reviewed bootstrap config branch does not match the bundle manifest");
@@ -7290,8 +7299,12 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
7290
7299
  const config = readBootstrapConfig(request.platformConfigFile);
7291
7300
  if (config.kind !== "platform")
7292
7301
  throw new Error("operator bootstrap requires a platform config");
7293
- const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
7294
- const staged = await stageConfig(config, directory);
7302
+ const secrets = mode === "apply" || mode === "repair" ? validatePlatformBootstrapSecrets(config, options.secrets, {
7303
+ allowStoredCloudflareCredentials: mode === "repair"
7304
+ }) : undefined;
7305
+ const staged = await stageConfig(config, directory, {
7306
+ reuseBoundCloudflare: mode === "repair"
7307
+ });
7295
7308
  await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
7296
7309
  await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
7297
7310
  for (const name of staged.files)
@@ -7302,7 +7315,14 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
7302
7315
  await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
7303
7316
  for (const { name } of staged.bundleFiles)
7304
7317
  await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0644", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap bundle", true);
7305
- const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
7318
+ const command = [
7319
+ "/usr/bin/sudo",
7320
+ "-n",
7321
+ "/usr/local/bin/fz",
7322
+ "bootstrap",
7323
+ mode === "repair" ? "repair" : "platform",
7324
+ "credentials-stdin"
7325
+ ];
7306
7326
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
7307
7327
  const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
7308
7328
  ` : undefined, mode === "apply");
@@ -3020,7 +3020,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
3020
3020
  }
3021
3021
 
3022
3022
  // src/version.ts
3023
- var VERSION3 = "0.1.124";
3023
+ var VERSION3 = "0.1.128";
3024
3024
 
3025
3025
  // src/egress-policy.ts
3026
3026
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/provision.js CHANGED
@@ -3020,7 +3020,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
3020
3020
  }
3021
3021
 
3022
3022
  // src/version.ts
3023
- var VERSION3 = "0.1.124";
3023
+ var VERSION3 = "0.1.128";
3024
3024
 
3025
3025
  // src/egress-policy.ts
3026
3026
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -4,4 +4,13 @@ export interface RecoveryHostResult {
4
4
  stderr: string;
5
5
  }
6
6
  export type RecoveryHostExec = (argv: readonly string[]) => Promise<RecoveryHostResult>;
7
+ export interface RecoveryBootstrapCoordinates {
8
+ profile: string;
9
+ role: string;
10
+ software: string;
11
+ apiOrigin: string;
12
+ edgeHostname: string;
13
+ databaseAddress?: string;
14
+ }
15
+ export declare function validateRecoveryBootstrapState(value: unknown, expected: RecoveryBootstrapCoordinates): void;
7
16
  export declare function runRecoveryHost(args: readonly string[], run?: RecoveryHostExec): Promise<void>;
@@ -28,6 +28,26 @@ var nonEmpty = (path) => {
28
28
  if (!existsSync(path) || !statSync(path).isFile() || statSync(path).size < 1)
29
29
  throw new Error(`required file missing: ${path}`);
30
30
  };
31
+ function validateRecoveryBootstrapState(value, expected) {
32
+ if (!value || typeof value !== "object" || Array.isArray(value))
33
+ throw new Error("bootstrap state is malformed");
34
+ const state = value;
35
+ if (state.format !== 2 || state.kind !== "platform" || state.environment !== expected.profile || state.databaseRole !== expected.role || state.profile !== expected.software || state.apiUrl !== expected.apiOrigin || state.nodeHostname !== expected.edgeHostname || expected.databaseAddress !== undefined && state.databaseAddress !== expected.databaseAddress)
36
+ throw new Error("bootstrap state does not match reviewed recovery coordinates");
37
+ }
38
+ var exactBootstrapState = (expected) => {
39
+ const path = "/var/lib/forgezero/bootstrap.json";
40
+ nonEmpty(path);
41
+ if (statSync(path).size > 64 * 1024)
42
+ throw new Error("bootstrap state is oversized");
43
+ let value;
44
+ try {
45
+ value = JSON.parse(readFileSync(path, "utf8"));
46
+ } catch {
47
+ throw new Error("bootstrap state is malformed");
48
+ }
49
+ validateRecoveryBootstrapState(value, expected);
50
+ };
31
51
  var option = (args, name) => {
32
52
  const value = args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
33
53
  if (!value || /[\0\r\n]/.test(value))
@@ -51,15 +71,11 @@ async function runRecoveryHost(args, run = execute) {
51
71
  const apiOrigin = option(args, "api-origin");
52
72
  const edgeHostname = option(args, "edge-hostname");
53
73
  const databaseAddress = args.find((arg) => arg.startsWith("--db-address="))?.slice(13);
54
- exactState("/opt/forgezero/.forge-state", `profile=${profile}`);
55
- exactState("/opt/forgezero/.forge-state", `db_role=${role}`);
56
- exactState("/opt/forgezero/.forge-state", `software_profile=${software}`);
57
- if (databaseAddress)
58
- exactState("/opt/forgezero/.forge-state", `db_address=${databaseAddress}`);
59
- exactState("/opt/forgezero/shared/.env", "ARANGO_DB=fz");
60
- exactState("/opt/forgezero/shared/.env", "FZ_DATABASE_MODE=platform");
61
- exactState("/opt/forgezero/shared/.env", `API_ORIGIN=${apiOrigin}`);
62
- exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME=${edgeHostname}`);
74
+ exactBootstrapState({ profile, role, software, apiOrigin, edgeHostname, ...databaseAddress ? { databaseAddress } : {} });
75
+ exactState("/opt/forgezero/shared/.env", 'ARANGO_DB="fz"');
76
+ exactState("/opt/forgezero/shared/.env", 'FZ_DATABASE_MODE="platform"');
77
+ exactState("/opt/forgezero/shared/.env", `API_ORIGIN="${apiOrigin}"`);
78
+ exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME="${edgeHostname}"`);
63
79
  nonEmpty("/etc/forgezero/creds/arangodb-jwt.cred");
64
80
  const hasDatabase = role !== "none";
65
81
  if (hasDatabase) {
@@ -103,7 +119,7 @@ async function runRecoveryHost(args, run = execute) {
103
119
  throw new Error("maintenance accepts only fz db argv");
104
120
  const slot = activeSlot();
105
121
  const properties = [
106
- "--property=User=forgezero",
122
+ "--property=User=forgezero-api",
107
123
  `--property=WorkingDirectory=/opt/forgezero/slots/${slot}`,
108
124
  "--property=Environment=NODE_ENV=production",
109
125
  "--property=EnvironmentFile=/opt/forgezero/shared/.env",
@@ -120,5 +136,6 @@ async function runRecoveryHost(args, run = execute) {
120
136
  throw new Error("unknown recovery-host action");
121
137
  }
122
138
  export {
139
+ validateRecoveryBootstrapState,
123
140
  runRecoveryHost
124
141
  };
@@ -10,6 +10,7 @@ export interface RemoteSshBootstrapClaim {
10
10
  realm: string;
11
11
  projectKey: string;
12
12
  environmentKey: string;
13
+ runnerSshPublicKey: string;
13
14
  target: {
14
15
  host: string;
15
16
  port: number;
@@ -25,7 +26,12 @@ export interface RemoteSshBootstrapClaim {
25
26
  source: 'runner';
26
27
  } | {
27
28
  source: 'vault';
29
+ kind: 'private-key';
28
30
  privateKey: string;
31
+ } | {
32
+ source: 'vault';
33
+ kind: 'password';
34
+ password: string;
29
35
  };
30
36
  };
31
37
  enrolmentToken: string;
@@ -42,6 +48,7 @@ export interface RemoteMetalAdmissionClaim {
42
48
  label: string;
43
49
  regionKey: string;
44
50
  runnerNodeKey: string;
51
+ runnerSshPublicKey: string;
45
52
  challenge: string;
46
53
  provisioning: {
47
54
  volumeGroup: string;
@@ -109,6 +116,7 @@ export interface BootstrapCommandResult {
109
116
  export type BootstrapCommand = (argv: readonly string[], options?: {
110
117
  stdin?: string;
111
118
  secret?: boolean;
119
+ env?: Record<string, string>;
112
120
  }) => Promise<BootstrapCommandResult>;
113
121
  export interface SshBootstrapPullOptions {
114
122
  apiUrl: string;
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.124";
2
+ export declare const VERSION = "0.1.128";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.124",
3
+ "version": "0.1.128",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -16,8 +16,8 @@
16
16
  "@noble/post-quantum": "^0.6.1"
17
17
  },
18
18
  "dependencies": {
19
- "@forgezero/runtime": "0.1.18",
20
- "@forgezero/vault": "0.1.24",
19
+ "@forgezero/runtime": "0.1.19",
20
+ "@forgezero/vault": "0.1.25",
21
21
  "@noble/curves": "2.2.0",
22
22
  "@noble/hashes": "2.2.0",
23
23
  "@noble/post-quantum": "0.6.1",