@forgezero/agent 0.1.125 → 0.1.129

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.125";
369
+ var VERSION = "0.1.129";
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' | 'promote' | '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.125";
1463
+ var VERSION = "0.1.129";
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)
@@ -4756,10 +4757,16 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4756
4757
  }
4757
4758
  allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
4758
4759
  }
4759
- const platformPrivate = config.kind === "platform" ? (() => {
4760
+ const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
4761
+ if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
4762
+ throw new Error("stored-credential promotion requires an installed enrolled platform compute");
4763
+ }
4764
+ const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
4760
4765
  if (!secrets)
4761
4766
  throw new Error("platform apply requires attended credentials on stdin");
4762
- const checked4 = validatePlatformBootstrapSecrets(config, secrets);
4767
+ const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
4768
+ allowStoredCloudflareCredentials: Boolean(installed?.cloudflare && config.cloudflareHandoff && !cloudflare)
4769
+ });
4763
4770
  return {
4764
4771
  root: checked4.clusterBootstrapCode,
4765
4772
  email: checked4.emailSecret,
@@ -4797,7 +4804,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4797
4804
  if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
4798
4805
  await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
4799
4806
  }
4800
- if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4807
+ if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4801
4808
  await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
4802
4809
  }
4803
4810
  const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
@@ -4855,24 +4862,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4855
4862
  await verifyBootstrapBundleOnHost(host, config, true);
4856
4863
  }
4857
4864
  if (config.kind === "platform") {
4858
- const root = platformPrivate.root;
4859
- if (!host.exists(JWT_CREDENTIAL)) {
4860
- await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4861
- }
4862
- if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4863
- await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4864
- }
4865
- await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4866
- await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4867
4865
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4868
- for (const [name, source] of Object.entries({
4869
- ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4870
- "backup.s3.secretAccessKey": platformPrivate.backup
4871
- })) {
4872
- if (source) {
4873
- const destination2 = `${CREDS}/${name}.cred`;
4874
- if (!host.exists(destination2)) {
4875
- await seal(host, name, destination2, source);
4866
+ if (reuseBoundPlatformCredentials) {
4867
+ const required = [
4868
+ ...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
4869
+ SEED_CREDENTIAL,
4870
+ BACKUP_RECOVERY_CREDENTIAL,
4871
+ ...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
4872
+ ...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
4873
+ ...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
4874
+ ...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
4875
+ ];
4876
+ const missing = required.find((path) => !host.exists(path));
4877
+ if (missing)
4878
+ throw new Error(`stored platform credential is missing: ${missing}`);
4879
+ } else {
4880
+ const root = platformPrivate.root;
4881
+ if (!host.exists(JWT_CREDENTIAL)) {
4882
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4883
+ }
4884
+ if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4885
+ await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4886
+ }
4887
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4888
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4889
+ for (const [name, source] of Object.entries({
4890
+ ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4891
+ "backup.s3.secretAccessKey": platformPrivate.backup
4892
+ })) {
4893
+ if (source) {
4894
+ const destination2 = `${CREDS}/${name}.cred`;
4895
+ if (!host.exists(destination2))
4896
+ await seal(host, name, destination2, source);
4876
4897
  }
4877
4898
  }
4878
4899
  }
@@ -5037,7 +5058,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
5037
5058
  host.remove(config.cloudflareHandoff.handoffFile);
5038
5059
  }
5039
5060
  let launch;
5040
- if (config.kind === "platform" && config.database.role === "master") {
5061
+ if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
5041
5062
  const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
5042
5063
  if (!host.exists(invitePath))
5043
5064
  throw new Error("platform invite is missing after deployment");
@@ -6713,12 +6734,12 @@ function planOperatorMetalBootstrap(request, mode) {
6713
6734
  var secretSources = (config) => [
6714
6735
  ...config.cloudflareHandoff ? [["cloudflare-handoff", config.cloudflareHandoff.handoffFile]] : []
6715
6736
  ];
6716
- var attendedSecretNames = (config) => [
6737
+ var attendedSecretNames = (config, options = {}) => [
6717
6738
  "cluster-bootstrap-code",
6718
6739
  config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : "fz_smtp.password",
6719
6740
  ...config.enrolment.source === "api-token" ? ["enrol-token"] : [],
6720
6741
  ...config.runtime.environment.backup ? ["backup.s3.secretAccessKey"] : [],
6721
- ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
6742
+ ...!options.reuseBoundCloudflare && (config.cloudflareHandoff || config.runtime.environment.cloudflare) ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
6722
6743
  ];
6723
6744
  function planOperatorPlatformBootstrap(request, mode) {
6724
6745
  const config = readBootstrapConfig(request.platformConfigFile);
@@ -6734,7 +6755,14 @@ function planOperatorPlatformBootstrap(request, mode) {
6734
6755
  user: request.target.user,
6735
6756
  ...request.target.jump ? { via: request.target.jump.address } : {}
6736
6757
  },
6737
- steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
6758
+ steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
6759
+ "verify pinned SSH transport and caller-approved SSH agent",
6760
+ "copy pinned Bun and packaged fz artifacts to a private staging directory",
6761
+ "verify and copy the one immutable API Git bundle plus its manifest",
6762
+ "stage the rewritten owner-only platform config without plaintext credentials",
6763
+ "promote the exact release using the credentials already sealed on the enrolled compute",
6764
+ "remove transient local and remote staging data"
6765
+ ] : [
6738
6766
  "verify pinned SSH transport and caller-approved SSH agent",
6739
6767
  "copy pinned Bun and packaged fz artifacts to a private staging directory",
6740
6768
  "verify and copy the one immutable API Git bundle plus its manifest",
@@ -6742,7 +6770,10 @@ function planOperatorPlatformBootstrap(request, mode) {
6742
6770
  "run typed fz bootstrap platform --apply from the local bundle",
6743
6771
  "remove transient local and remote staging data"
6744
6772
  ],
6745
- secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
6773
+ secretInputs: mode === "apply" || mode === "repair" ? [
6774
+ ...attendedSecretNames(config, { reuseBoundCloudflare: mode === "repair" }),
6775
+ ...mode === "repair" ? [] : secretSources(config).map(([name]) => name)
6776
+ ] : []
6746
6777
  };
6747
6778
  }
6748
6779
  var fleetConfigIdentity = (config) => {
@@ -7172,10 +7203,10 @@ async function withOperatorGuestTransport(request, evidence, callback, options =
7172
7203
  rmSync6(directory, { recursive: true, force: true });
7173
7204
  }
7174
7205
  }
7175
- async function stageConfig(config, directory) {
7206
+ async function stageConfig(config, directory, options = {}) {
7176
7207
  const rewritten = structuredClone(config);
7177
7208
  const staged = [];
7178
- for (const [name, source] of secretSources(config)) {
7209
+ for (const [name, source] of options.reuseBoundCloudflare ? [] : secretSources(config)) {
7179
7210
  const bytes = ownerFile(source, SECRET_LIMIT, name);
7180
7211
  const local = join6(directory, name);
7181
7212
  writeFileSync8(local, bytes, { mode: 384, flag: "wx" });
@@ -7184,6 +7215,9 @@ async function stageConfig(config, directory) {
7184
7215
  if (name === "cloudflare-handoff")
7185
7216
  rewritten.cloudflareHandoff.handoffFile = remotePath;
7186
7217
  }
7218
+ if (options.reuseBoundCloudflare && rewritten.cloudflareHandoff) {
7219
+ rewritten.cloudflareHandoff.handoffFile = `${REMOTE_STAGE}/cloudflare-handoff`;
7220
+ }
7187
7221
  const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
7188
7222
  if (bundle.manifest.branch !== config.bootstrapBundle.branch) {
7189
7223
  throw new Error("reviewed bootstrap config branch does not match the bundle manifest");
@@ -7290,8 +7324,12 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
7290
7324
  const config = readBootstrapConfig(request.platformConfigFile);
7291
7325
  if (config.kind !== "platform")
7292
7326
  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);
7327
+ const secrets = mode === "apply" || mode === "repair" ? validatePlatformBootstrapSecrets(config, options.secrets, {
7328
+ allowStoredCloudflareCredentials: mode === "repair"
7329
+ }) : undefined;
7330
+ const staged = await stageConfig(config, directory, {
7331
+ reuseBoundCloudflare: mode === "repair" || mode === "promote"
7332
+ });
7295
7333
  await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
7296
7334
  await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
7297
7335
  for (const name of staged.files)
@@ -7302,7 +7340,14 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
7302
7340
  await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
7303
7341
  for (const { name } of staged.bundleFiles)
7304
7342
  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"];
7343
+ const command = [
7344
+ "/usr/bin/sudo",
7345
+ "-n",
7346
+ "/usr/local/bin/fz",
7347
+ "bootstrap",
7348
+ mode === "apply" ? "platform" : "repair",
7349
+ mode === "promote" ? "stored-credentials" : "credentials-stdin"
7350
+ ];
7306
7351
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
7307
7352
  const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
7308
7353
  ` : 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.125";
3023
+ var VERSION3 = "0.1.129";
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.125";
3023
+ var VERSION3 = "0.1.129";
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
  };
@@ -1,6 +1,7 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
2
  import type { AgentOperationTelemetry } from './telemetry-runtime';
3
3
  import { type GitHostResolver } from './git-egress';
4
+ import type { MetalBootstrapConfig } from './metal-bootstrap';
4
5
  export interface RemoteSshBootstrapClaim {
5
6
  computeKey: string;
6
7
  computeReference: string;
@@ -10,6 +11,7 @@ export interface RemoteSshBootstrapClaim {
10
11
  realm: string;
11
12
  projectKey: string;
12
13
  environmentKey: string;
14
+ runnerSshPublicKey: string;
13
15
  target: {
14
16
  host: string;
15
17
  port: number;
@@ -25,7 +27,12 @@ export interface RemoteSshBootstrapClaim {
25
27
  source: 'runner';
26
28
  } | {
27
29
  source: 'vault';
30
+ kind: 'private-key';
28
31
  privateKey: string;
32
+ } | {
33
+ source: 'vault';
34
+ kind: 'password';
35
+ password: string;
29
36
  };
30
37
  };
31
38
  enrolmentToken: string;
@@ -42,31 +49,18 @@ export interface RemoteMetalAdmissionClaim {
42
49
  label: string;
43
50
  regionKey: string;
44
51
  runnerNodeKey: string;
52
+ runnerSshPublicKey: string;
45
53
  challenge: string;
46
- provisioning: {
47
- volumeGroup: string;
48
- bridge: string;
49
- subnetPrefix: string;
50
- addressStart: number;
51
- addressEnd: number;
52
- gateway: string;
53
- nameservers: string[];
54
- cpuPools: Array<{
55
- key: string;
56
- cpus: string;
57
- physicalCores: number;
58
- memoryNodes?: string;
59
- }>;
60
- housekeepingCpus: string;
61
- housekeepingMemoryNodes?: string;
62
- confidential?: {
63
- cbitpos: number;
64
- reducedPhysBits: number;
65
- policy: string;
66
- };
67
- };
68
54
  target: RemoteSshBootstrapClaim['target'];
69
55
  }
56
+ type MetalAdmissionProvisioning = Pick<MetalBootstrapConfig['profile'], 'volumeGroup' | 'bridge' | 'subnetPrefix' | 'addressStart' | 'addressEnd' | 'gateway' | 'nameservers' | 'cpuPools' | 'housekeepingCpus'>;
57
+ /**
58
+ * Convert runner-observed Linux CPU topology into the fixed ForgeZero host
59
+ * boundary. The browser never authors this capacity. One physical core and all
60
+ * of its SMT siblings remain reserved for the host; every other complete core
61
+ * becomes the initial allocatable pool.
62
+ */
63
+ export declare function deriveMetalAdmissionProvisioning(lscpu: string): MetalAdmissionProvisioning;
70
64
  export interface MetalAdmissionProof {
71
65
  jobKey: string;
72
66
  tenantKey: string;
@@ -109,6 +103,7 @@ export interface BootstrapCommandResult {
109
103
  export type BootstrapCommand = (argv: readonly string[], options?: {
110
104
  stdin?: string;
111
105
  secret?: boolean;
106
+ env?: Record<string, string>;
112
107
  }) => Promise<BootstrapCommandResult>;
113
108
  export interface SshBootstrapPullOptions {
114
109
  apiUrl: string;
@@ -173,3 +168,4 @@ export declare function startMetalAdmissionPull(options: SshBootstrapPullOptions
173
168
  stop(): Promise<void>;
174
169
  readonly active: boolean;
175
170
  };
171
+ export {};
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.125";
2
+ export declare const VERSION = "0.1.129";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.125",
3
+ "version": "0.1.129",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -11,13 +11,13 @@
11
11
  "typescript": "^5.6.0",
12
12
  "@types/bun": "latest",
13
13
  "@types/node": "^22.0.0",
14
- "@forgezero/access": "0.1.13",
14
+ "@forgezero/access": "0.1.14",
15
15
  "@noble/curves": "^2.2.0",
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.20",
20
+ "@forgezero/vault": "0.1.26",
21
21
  "@noble/curves": "2.2.0",
22
22
  "@noble/hashes": "2.2.0",
23
23
  "@noble/post-quantum": "0.6.1",