@forgezero/agent 0.1.101 → 0.1.103

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.
@@ -917,7 +917,7 @@ async function postSignedNode(options, path, body) {
917
917
  }
918
918
 
919
919
  // src/version.ts
920
- var VERSION3 = "0.1.101";
920
+ var VERSION3 = "0.1.103";
921
921
 
922
922
  // src/agent-heartbeat.ts
923
923
  function readAgentHostMetrics() {
@@ -115,6 +115,10 @@ export interface PlatformBootstrapSecrets {
115
115
  backupS3Secret?: string;
116
116
  cloudflareTunnelToken?: string;
117
117
  cloudflareApiToken?: string;
118
+ githubClientSecret?: string;
119
+ /** One-line base64 form accepted by prompts/env; decoded before systemd sealing. */
120
+ githubPrivateKeyBase64?: string;
121
+ githubWebhookSecret?: string;
118
122
  }
119
123
  export interface EnrolledComputeBootstrapSecrets {
120
124
  enrolmentToken: string;
@@ -195,8 +199,8 @@ export declare const legacyBootstrapIdentityDigest: (config: BootstrapConfig) =>
195
199
  /**
196
200
  * Exact, reviewed identity migrations for an already-installed platform host.
197
201
  * This is deliberately not a generic "stored coordinates match" escape hatch:
198
- * only the development collector transition and the addition of reviewed
199
- * genesis attestation evidence are accepted.
202
+ * only the development collector transition, reviewed genesis evidence, and
203
+ * narrowly identified bootstrap-schema repairs are accepted.
200
204
  */
201
205
  export declare function approvedPlatformRepairIdentityDigests(config: PlatformBootstrapConfig): readonly string[];
202
206
  /** Exact prior-release identities accepted only while generation one is still unenrolled. */
package/dist/bootstrap.js CHANGED
@@ -1386,7 +1386,7 @@ async function buildBootstrapBundle(input, exec = run) {
1386
1386
  }
1387
1387
 
1388
1388
  // src/bootstrap.ts
1389
- import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
1389
+ import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
1390
1390
  import {
1391
1391
  chmodSync as chmodSync3,
1392
1392
  existsSync as existsSync5,
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1420
1420
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1421
1421
 
1422
1422
  // src/version.ts
1423
- var VERSION = "0.1.101";
1423
+ var VERSION = "0.1.103";
1424
1424
 
1425
1425
  // src/software.ts
1426
1426
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3090,10 +3090,16 @@ var httpsOrigin = (name, raw) => {
3090
3090
  };
3091
3091
  var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
3092
3092
  function validatePlatformInitialInventory(value) {
3093
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "attestation", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
3093
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
3094
3094
  throw new Error("initial platform inventory coordinates are invalid");
3095
3095
  }
3096
3096
  safeAtom("initialInventory.region.label", value.region.label);
3097
+ if (value.metalIdentity !== undefined) {
3098
+ const identity = value.metalIdentity;
3099
+ if (!identity || typeof identity !== "object" || Array.isArray(identity) || Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !identity.publicKeys || identity.publicKeys.ed25519 !== identity.nodeKey || !/^[A-Za-z0-9_-]{1,8192}$/.test(identity.publicKeys.mlDsa) || Object.keys(identity.publicKeys).some((key) => !["ed25519", "mlDsa"].includes(key))) {
3100
+ throw new Error("initial platform Metal identity is invalid");
3101
+ }
3102
+ }
3097
3103
  if (value.region.city !== undefined)
3098
3104
  safeAtom("initialInventory.region.city", value.region.city);
3099
3105
  if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
@@ -3128,6 +3134,7 @@ function validatePlatformInitialInventory(value) {
3128
3134
  }
3129
3135
  return {
3130
3136
  metalHostname: value.metalHostname,
3137
+ ...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
3131
3138
  region: { ...value.region },
3132
3139
  computes: value.computes.map((compute, index) => ({
3133
3140
  ...computes[index],
@@ -3181,6 +3188,11 @@ function validatePlatformSharedEnvironment(input) {
3181
3188
  } else if (input.email !== undefined) {
3182
3189
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
3183
3190
  }
3191
+ if (input.githubApp) {
3192
+ if (!/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubApp.clientId) || !/^[1-9][0-9]{0,19}$/.test(input.githubApp.appId) || !/^[a-z0-9][a-z0-9-]{0,99}$/.test(input.githubApp.slug)) {
3193
+ throw new Error("GitHub App client id, app id or slug is malformed.");
3194
+ }
3195
+ }
3184
3196
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
3185
3197
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
3186
3198
  throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
@@ -3311,6 +3323,9 @@ function renderPlatformSharedEnvironment(input) {
3311
3323
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
3312
3324
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
3313
3325
  FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
3326
+ FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
3327
+ FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
3328
+ FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
3314
3329
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
3315
3330
  };
3316
3331
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -3322,6 +3337,9 @@ function platformApiCredentialSpecs(options) {
3322
3337
  const optional = [
3323
3338
  ["fz_smtp.password", options.emailProvider === "smtp"],
3324
3339
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
3340
+ ["fz_github.clientSecret", options.githubApp],
3341
+ ["fz_github.privateKey", options.githubApp],
3342
+ ["fz_github.webhookSecret", options.githubApp],
3325
3343
  ["CF_API_TOKEN", options.cloudflareKv],
3326
3344
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
3327
3345
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -3717,7 +3735,17 @@ function validatePlatformBootstrapSecrets(config, input) {
3717
3735
  if (!input || typeof input !== "object" || Array.isArray(input))
3718
3736
  throw new Error("bootstrap credential input must be an object");
3719
3737
  const source = input;
3720
- const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
3738
+ const allowed = [
3739
+ "clusterBootstrapCode",
3740
+ "emailSecret",
3741
+ "enrolmentToken",
3742
+ "backupS3Secret",
3743
+ "cloudflareTunnelToken",
3744
+ "cloudflareApiToken",
3745
+ "githubClientSecret",
3746
+ "githubPrivateKeyBase64",
3747
+ "githubWebhookSecret"
3748
+ ];
3721
3749
  const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
3722
3750
  if (unknown.length)
3723
3751
  throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
@@ -3727,6 +3755,9 @@ function validatePlatformBootstrapSecrets(config, input) {
3727
3755
  const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
3728
3756
  const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
3729
3757
  const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
3758
+ const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
3759
+ const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
3760
+ const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
3730
3761
  if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
3731
3762
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3732
3763
  if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
@@ -3746,12 +3777,29 @@ function validatePlatformBootstrapSecrets(config, input) {
3746
3777
  }
3747
3778
  if (cloudflareTunnelToken)
3748
3779
  validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
3780
+ const githubConfigured = Boolean(config.runtime.environment.githubApp);
3781
+ if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
3782
+ throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
3783
+ }
3784
+ if (githubConfigured) {
3785
+ if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
3786
+ throw new Error("GitHub App client or webhook secret is malformed");
3787
+ }
3788
+ try {
3789
+ const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
3790
+ if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
3791
+ throw new Error("not RSA");
3792
+ } catch {
3793
+ throw new Error("GitHub App private key must be a base64-encoded RSA private key");
3794
+ }
3795
+ }
3749
3796
  return {
3750
3797
  clusterBootstrapCode,
3751
3798
  emailSecret,
3752
3799
  ...enrolmentToken ? { enrolmentToken } : {},
3753
3800
  ...backupS3Secret ? { backupS3Secret } : {},
3754
- ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
3801
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
3802
+ ...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
3755
3803
  };
3756
3804
  }
3757
3805
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
@@ -4303,6 +4351,17 @@ function approvedPlatformRepairIdentityDigests(config) {
4303
4351
  delete beforeAttestation.runtime.environment.initialInventory.attestation;
4304
4352
  prior.push(beforeAttestation);
4305
4353
  }
4354
+ const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
4355
+ if (metalIdentity && config.enrolment.source === "genesis-derived") {
4356
+ const beforeMetalIdentity = structuredClone(config);
4357
+ delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
4358
+ prior.push(beforeMetalIdentity);
4359
+ }
4360
+ if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
4361
+ const beforeReplicaCustodianEmail = structuredClone(config);
4362
+ delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
4363
+ prior.push(beforeReplicaCustodianEmail);
4364
+ }
4306
4365
  const telemetryMigration = config.environment === "development" && config.telemetryEndpoint === LOCAL_DEBUG_OTEL_EXPORT && config.runtime.environment.agentOtlpEndpoint === "http://127.0.0.1:4318";
4307
4366
  if (telemetryMigration) {
4308
4367
  const beforeTelemetry = structuredClone(config);
@@ -4655,7 +4714,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4655
4714
  enrolmentToken: checked4.enrolmentToken,
4656
4715
  backup: checked4.backupS3Secret,
4657
4716
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
4658
- cloudflareApiToken: checked4.cloudflareApiToken
4717
+ cloudflareApiToken: checked4.cloudflareApiToken,
4718
+ githubClientSecret: checked4.githubClientSecret,
4719
+ githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
4720
+ githubWebhookSecret: checked4.githubWebhookSecret
4659
4721
  };
4660
4722
  })() : undefined;
4661
4723
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -4753,6 +4815,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4753
4815
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4754
4816
  for (const [name, source] of Object.entries({
4755
4817
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4818
+ "fz_github.clientSecret": platformPrivate.githubClientSecret,
4819
+ "fz_github.privateKey": platformPrivate.githubPrivateKey,
4820
+ "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
4756
4821
  "backup.s3.secretAccessKey": platformPrivate.backup
4757
4822
  })) {
4758
4823
  if (source) {
@@ -4769,6 +4834,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4769
4834
  const credentials = platformApiCredentialSpecs({
4770
4835
  emailProvider: runtime.environment.email?.provider,
4771
4836
  cloudflareKv: cloudflareConfigured,
4837
+ githubApp: Boolean(runtime.environment.githubApp),
4772
4838
  realtime: Boolean(runtime.environment.realtime)
4773
4839
  });
4774
4840
  const units = renderPlatformApiUnits({
package/dist/fz-agent.js CHANGED
@@ -9496,7 +9496,7 @@ async function writeAndCloseProcessInput(input, value) {
9496
9496
  }
9497
9497
 
9498
9498
  // src/version.ts
9499
- var VERSION2 = "0.1.101";
9499
+ var VERSION2 = "0.1.103";
9500
9500
 
9501
9501
  // src/ssh-bootstrap.ts
9502
9502
  class SshBootstrapError extends Error {