@forgezero/agent 0.1.35 → 0.1.36

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.
package/dist/fz.js CHANGED
@@ -4875,7 +4875,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4875
4875
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4876
4876
 
4877
4877
  // src/version.ts
4878
- var VERSION2 = "0.1.35";
4878
+ var VERSION2 = "0.1.36";
4879
4879
 
4880
4880
  // src/software.ts
4881
4881
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
@@ -4889,7 +4889,8 @@ var SOFTWARE_CATALOG = [
4889
4889
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4890
4890
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4891
4891
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4892
- { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4892
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4893
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4893
4894
  ];
4894
4895
  var UBUNTU_2604_X64 = [
4895
4896
  {
@@ -4916,6 +4917,11 @@ var UBUNTU_2604_X64 = [
4916
4917
  requirement: { id: "ufw", version: "ubuntu-26.04" },
4917
4918
  check: "command -v ufw >/dev/null",
4918
4919
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
4920
+ },
4921
+ {
4922
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
4923
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
4924
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
4919
4925
  }
4920
4926
  ];
4921
4927
  function validateSoftwareRequirements(value, _options = {}) {
@@ -4929,7 +4935,7 @@ function validateSoftwareRequirements(value, _options = {}) {
4929
4935
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
4930
4936
  throw new Error("software requirement contains an unknown field");
4931
4937
  }
4932
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4938
+ if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4933
4939
  throw new Error("software requirement coordinate is invalid");
4934
4940
  }
4935
4941
  const requirement = { id: row.id, version: row.version };
@@ -5482,7 +5488,12 @@ function agentUnit(options) {
5482
5488
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
5483
5489
  throw new Error("migration pull and lifecycle profile must be supplied together");
5484
5490
  }
5491
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
5492
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
5493
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
5494
+ }
5485
5495
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
5496
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
5486
5497
  const warpValues = [
5487
5498
  options.warpOrganization,
5488
5499
  options.warpClientIdCredentialPath,
@@ -5548,6 +5559,9 @@ function agentUnit(options) {
5548
5559
  options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
5549
5560
  options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
5550
5561
  options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
5562
+ options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
5563
+ options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
5564
+ options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
5551
5565
  `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
5552
5566
  options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
5553
5567
  ].filter((line) => line !== null);
@@ -5555,6 +5569,8 @@ function agentUnit(options) {
5555
5569
  environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
5556
5570
  }
5557
5571
  const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
5572
+ ` : "";
5573
+ const bootstrapCredential = bootstrapEnabled ? `LoadCredentialEncrypted=bootstrap-ssh-key:${bootstrapSshCredentialPath}
5558
5574
  ` : "";
5559
5575
  const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
5560
5576
  `);
@@ -5609,7 +5625,7 @@ User=${user}
5609
5625
  Group=${VAULT_GROUP}
5610
5626
  ${deploymentGroup}
5611
5627
  LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
5612
- ${gitCredential}${projectCredentials}${projectCredentials ? `
5628
+ ${gitCredential}${bootstrapCredential}${projectCredentials}${projectCredentials ? `
5613
5629
  ` : ""}${snpPrepare}ExecStart=${bin}
5614
5630
  Restart=always
5615
5631
  RestartSec=2
@@ -5664,6 +5680,10 @@ function planProvision(options) {
5664
5680
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
5665
5681
  throw new Error("migration pull and lifecycle profile must be supplied together");
5666
5682
  }
5683
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
5684
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
5685
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
5686
+ }
5667
5687
  const warpValues = [
5668
5688
  options.warpOrganization,
5669
5689
  options.warpClientIdCredentialPath,
@@ -5689,13 +5709,20 @@ function planProvision(options) {
5689
5709
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
5690
5710
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
5691
5711
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
5712
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
5692
5713
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
5693
5714
  const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
5694
5715
  return {
5695
5716
  mode,
5696
5717
  reason: reasonFor(mode),
5697
5718
  unitPath: UNIT_PATH,
5698
- unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
5719
+ unit: agentUnit({
5720
+ ...options,
5721
+ mode,
5722
+ lifecycleProfilePath,
5723
+ lifecycleHelperSocketPath,
5724
+ bootstrapSshCredentialPath
5725
+ }),
5699
5726
  auxiliaryUnits: [
5700
5727
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
5701
5728
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
@@ -11639,6 +11666,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
11639
11666
  var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
11640
11667
  var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
11641
11668
  var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
11669
+ var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
11642
11670
  var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
11643
11671
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
11644
11672
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
@@ -11701,6 +11729,20 @@ function validateBootstrapConfig(value) {
11701
11729
  }
11702
11730
  if (!value.enrolTokenFile)
11703
11731
  throw new Error("tenant bootstrap requires --enrol-token-file");
11732
+ if (value.bootstrapRunner) {
11733
+ if (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile)) {
11734
+ throw new Error("bootstrap runner SSH private-key file must be absolute");
11735
+ }
11736
+ let targetTelemetry;
11737
+ try {
11738
+ targetTelemetry = new URL(value.bootstrapRunner.targetTelemetryEndpoint);
11739
+ } catch {
11740
+ throw new Error("bootstrap target telemetry endpoint is malformed");
11741
+ }
11742
+ if (targetTelemetry.protocol !== "https:" || targetTelemetry.username || targetTelemetry.password || targetTelemetry.search || targetTelemetry.hash || targetTelemetry.port && targetTelemetry.port !== "443") {
11743
+ throw new Error("bootstrap target telemetry must be public HTTPS on port 443");
11744
+ }
11745
+ }
11704
11746
  return value;
11705
11747
  }
11706
11748
  if (!PLATFORM_BOOTSTRAP_PROFILES.includes(value.profile))
@@ -11762,7 +11804,11 @@ function validateBootstrapConfig(value) {
11762
11804
  }
11763
11805
  function planBootstrap(input, initialized = false) {
11764
11806
  const config = validateBootstrapConfig(structuredClone(input));
11765
- const software = config.kind === "tenant" ? [...config.software ?? [], ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []] : [
11807
+ const software = config.kind === "tenant" ? [
11808
+ ...config.software ?? [],
11809
+ ...config.bootstrapRunner && !config.software?.some(({ id: id2 }) => id2 === "openssh-client") ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
11810
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
11811
+ ] : [
11766
11812
  ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
11767
11813
  { id: "bun", version: "1.3.14" },
11768
11814
  { id: "nginx", version: "ubuntu-26.04" },
@@ -11997,6 +12043,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
11997
12043
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
11998
12044
  host.mkdir(CREDS, 448);
11999
12045
  host.mkdir("/var/lib/forgezero", 448);
12046
+ if (config.kind === "tenant" && config.bootstrapRunner && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
12047
+ await seal(host, "bootstrap-ssh-key", BOOTSTRAP_SSH_CREDENTIAL, privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key"));
12048
+ host.remove(config.bootstrapRunner.sshPrivateKeyFile);
12049
+ }
12000
12050
  if (cloudflare?.warp) {
12001
12051
  await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
12002
12052
  await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
@@ -12196,7 +12246,8 @@ function strictBootstrapDocument(value) {
12196
12246
  "cloudflareHandoff",
12197
12247
  "realm",
12198
12248
  "enrolTokenFile",
12199
- "software"
12249
+ "software",
12250
+ "bootstrapRunner"
12200
12251
  ], "bootstrap config");
12201
12252
  if (root.kind === "platform") {
12202
12253
  exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
@@ -12261,6 +12312,8 @@ function strictBootstrapDocument(value) {
12261
12312
  exactKeys2(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
12262
12313
  }
12263
12314
  } else if (root.kind === "tenant") {
12315
+ if (root.bootstrapRunner !== undefined)
12316
+ exactKeys2(root.bootstrapRunner, ["sshPrivateKeyFile", "targetTelemetryEndpoint"], "bootstrap runner config");
12264
12317
  for (const key of ["environment", "profile", "computeReference", "database", "platformEnrolTokenFile", "runtime", "cloudflareHandoff"]) {
12265
12318
  if (root[key] !== undefined && key !== "profile")
12266
12319
  throw new Error(`tenant bootstrap cannot contain ${key}`);
@@ -12355,6 +12408,9 @@ function localBootstrapHost() {
12355
12408
  generateGitIdentity: true,
12356
12409
  pullDeployments: true,
12357
12410
  pullMigrations: config.kind === "platform",
12411
+ pullBootstrap: config.kind === "tenant" && Boolean(config.bootstrapRunner),
12412
+ bootstrapSshCredentialPath: config.kind === "tenant" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
12413
+ bootstrapTargetTelemetryEndpoint: config.kind === "tenant" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
12358
12414
  lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
12359
12415
  ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
12360
12416
  warpOrganization: config.runtime.environment.cloudflare.warp.organization,
@@ -14140,6 +14196,7 @@ function interactiveBootstrap(kind) {
14140
14196
  if (!process.stdin.isTTY)
14141
14197
  throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
14142
14198
  if (kind === "tenant") {
14199
+ const runnerKey = bootstrapAnswer("Bootstrap runner SSH private-key file (blank to disable)", "");
14143
14200
  return {
14144
14201
  kind,
14145
14202
  apiUrl: bootstrapAnswer("Platform API URL"),
@@ -14147,7 +14204,11 @@ function interactiveBootstrap(kind) {
14147
14204
  nodeHostname: bootstrapAnswer("Node hostname", hostname()),
14148
14205
  telemetryEndpoint: bootstrapAnswer("Public HTTPS OTLP endpoint"),
14149
14206
  enrolTokenFile: bootstrapAnswer("Root-only enrolment token file"),
14150
- profile: bootstrapAnswer("Deploy profile", "app")
14207
+ profile: bootstrapAnswer("Deploy profile", "app"),
14208
+ ...runnerKey ? { bootstrapRunner: {
14209
+ sshPrivateKeyFile: runnerKey,
14210
+ targetTelemetryEndpoint: bootstrapAnswer("Public HTTPS OTLP endpoint for bootstrapped targets")
14211
+ } } : {}
14151
14212
  };
14152
14213
  }
14153
14214
  const environment = bootstrapAnswer("Environment (production/development)", "production");
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from '.
15
15
  export { pullProvisioningOnce, startProvisioningPull } from './provisioning-pull';
16
16
  export type { CreateRemoteProvisionClaim, ProvisioningPullOptions, RemoteProvisionClaim, GuestAccess, ProvisionPullResult, ProvisionRunner, ProvisionResult } from './provisioning-pull';
17
17
  export { pullMigrationOnce, startMigrationPull } from './migration-pull';
18
+ export { executeSshBootstrap, pullSshBootstrapOnce, startSshBootstrapPull } from './ssh-bootstrap';
19
+ export type { RemoteSshBootstrapClaim, SshBootstrapPullOptions, SshBootstrapResult } from './ssh-bootstrap';
18
20
  export type { MigrationAction, MigrationEvidence, MigrationNetwork, MigrationPullOptions, MigrationPullResult, RemoteMigrationClaim } from './migration-pull';
19
21
  export { enrolGuestIdentity, loadGuestBinding } from './guest-enrolment';
20
22
  export type { GuestBinding, GuestEnrolmentOptions } from './guest-enrolment';
@@ -293,7 +293,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
293
293
  }
294
294
 
295
295
  // src/version.ts
296
- var VERSION = "0.1.35";
296
+ var VERSION = "0.1.36";
297
297
 
298
298
  // src/metal-bootstrap.ts
299
299
  var PROFILE_PATH = "/etc/forgezero/metal.json";
@@ -100,6 +100,10 @@ export interface UnitOptions {
100
100
  runnerPublicTcpPorts?: readonly number[];
101
101
  /** Enable PQ-authenticated lifecycle claims through a constrained root helper. */
102
102
  pullMigrations?: boolean;
103
+ /** Claim API-owned SSH bootstrap jobs; the private key stays a systemd credential on this runner. */
104
+ pullBootstrap?: boolean;
105
+ bootstrapSshCredentialPath?: string;
106
+ bootstrapTargetTelemetryEndpoint?: string;
103
107
  /** Root-owned declarative lifecycle profile; never supplied by a migration claim. */
104
108
  lifecycleProfilePath?: string;
105
109
  lifecycleHelperSocketPath?: string;
package/dist/provision.js CHANGED
@@ -673,6 +673,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
673
673
 
674
674
  // src/software.ts
675
675
  import { readFileSync as readFileSync3 } from "node:fs";
676
+ var PINNED_BUN_VERSION = "1.3.14";
676
677
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
677
678
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
678
679
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
@@ -684,7 +685,8 @@ var SOFTWARE_CATALOG = [
684
685
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
685
686
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
686
687
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
687
- { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
688
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
689
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
688
690
  ];
689
691
  var UBUNTU_2604_X64 = [
690
692
  {
@@ -711,6 +713,11 @@ var UBUNTU_2604_X64 = [
711
713
  requirement: { id: "ufw", version: "ubuntu-26.04" },
712
714
  check: "command -v ufw >/dev/null",
713
715
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
716
+ },
717
+ {
718
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
719
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
720
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
714
721
  }
715
722
  ];
716
723
  function observeSoftwareHost(osRelease = readFileSync3("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -735,7 +742,7 @@ function validateSoftwareRequirements(value, _options = {}) {
735
742
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
736
743
  throw new Error("software requirement contains an unknown field");
737
744
  }
738
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
745
+ if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
739
746
  throw new Error("software requirement coordinate is invalid");
740
747
  }
741
748
  const requirement = { id: row.id, version: row.version };
@@ -884,7 +891,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
884
891
  }
885
892
 
886
893
  // src/version.ts
887
- var VERSION3 = "0.1.35";
894
+ var VERSION3 = "0.1.36";
888
895
 
889
896
  // src/egress-policy.ts
890
897
  import { realpathSync } from "node:fs";
@@ -1419,7 +1426,12 @@ function agentUnit(options) {
1419
1426
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1420
1427
  throw new Error("migration pull and lifecycle profile must be supplied together");
1421
1428
  }
1429
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1430
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1431
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1432
+ }
1422
1433
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1434
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
1423
1435
  const warpValues = [
1424
1436
  options.warpOrganization,
1425
1437
  options.warpClientIdCredentialPath,
@@ -1485,6 +1497,9 @@ function agentUnit(options) {
1485
1497
  options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
1486
1498
  options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
1487
1499
  options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
1500
+ options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
1501
+ options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
1502
+ options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
1488
1503
  `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
1489
1504
  options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
1490
1505
  ].filter((line) => line !== null);
@@ -1492,6 +1507,8 @@ function agentUnit(options) {
1492
1507
  environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
1493
1508
  }
1494
1509
  const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
1510
+ ` : "";
1511
+ const bootstrapCredential = bootstrapEnabled ? `LoadCredentialEncrypted=bootstrap-ssh-key:${bootstrapSshCredentialPath}
1495
1512
  ` : "";
1496
1513
  const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
1497
1514
  `);
@@ -1546,7 +1563,7 @@ User=${user}
1546
1563
  Group=${VAULT_GROUP}
1547
1564
  ${deploymentGroup}
1548
1565
  LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
1549
- ${gitCredential}${projectCredentials}${projectCredentials ? `
1566
+ ${gitCredential}${bootstrapCredential}${projectCredentials}${projectCredentials ? `
1550
1567
  ` : ""}${snpPrepare}ExecStart=${bin}
1551
1568
  Restart=always
1552
1569
  RestartSec=2
@@ -1601,6 +1618,10 @@ function planProvision(options) {
1601
1618
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1602
1619
  throw new Error("migration pull and lifecycle profile must be supplied together");
1603
1620
  }
1621
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1622
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1623
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1624
+ }
1604
1625
  const warpValues = [
1605
1626
  options.warpOrganization,
1606
1627
  options.warpClientIdCredentialPath,
@@ -1626,13 +1647,20 @@ function planProvision(options) {
1626
1647
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
1627
1648
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
1628
1649
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1650
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
1629
1651
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
1630
1652
  const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
1631
1653
  return {
1632
1654
  mode,
1633
1655
  reason: reasonFor(mode),
1634
1656
  unitPath: UNIT_PATH,
1635
- unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
1657
+ unit: agentUnit({
1658
+ ...options,
1659
+ mode,
1660
+ lifecycleProfilePath,
1661
+ lifecycleHelperSocketPath,
1662
+ bootstrapSshCredentialPath
1663
+ }),
1636
1664
  auxiliaryUnits: [
1637
1665
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
1638
1666
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
@@ -1,5 +1,6 @@
1
1
  // src/software.ts
2
2
  import { readFileSync } from "node:fs";
3
+ var PINNED_BUN_VERSION = "1.3.14";
3
4
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
4
5
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
5
6
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
@@ -11,7 +12,8 @@ var SOFTWARE_CATALOG = [
11
12
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
12
13
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
13
14
  { 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
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
16
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
17
  ];
16
18
  var UBUNTU_2604_X64 = [
17
19
  {
@@ -38,6 +40,11 @@ var UBUNTU_2604_X64 = [
38
40
  requirement: { id: "ufw", version: "ubuntu-26.04" },
39
41
  check: "command -v ufw >/dev/null",
40
42
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
43
+ },
44
+ {
45
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
46
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
47
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
41
48
  }
42
49
  ];
43
50
  function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -62,7 +69,7 @@ function validateSoftwareRequirements(value, _options = {}) {
62
69
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
63
70
  throw new Error("software requirement contains an unknown field");
64
71
  }
65
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
72
+ if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
66
73
  throw new Error("software requirement coordinate is invalid");
67
74
  }
68
75
  const requirement = { id: row.id, version: row.version };
@@ -1,6 +1,6 @@
1
1
  export type CatalogStatus = 'testing' | 'active' | 'retired';
2
2
  export type DeploymentChannel = 'development' | 'production';
3
- export type SoftwareId = 'bun' | 'nginx' | 'arangodb' | 'cloudflared' | 'ufw';
3
+ export type SoftwareId = 'bun' | 'nginx' | 'arangodb' | 'cloudflared' | 'ufw' | 'openssh-client';
4
4
  /** Repository input is a catalogue coordinate, never a root command. */
5
5
  export interface SoftwareRequirement {
6
6
  id: SoftwareId;
@@ -32,6 +32,8 @@ export interface SoftwareCommandResult {
32
32
  output: string;
33
33
  }
34
34
  export type SoftwareExec = (command: string) => Promise<SoftwareCommandResult>;
35
+ export declare const PINNED_BUN_VERSION = "1.3.14";
36
+ export declare const BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
35
37
  /** Public, command-free catalog. Root strategies remain private below. */
36
38
  export declare const OS_CATALOG: readonly OsCatalogEntry[];
37
39
  export declare const SOFTWARE_CATALOG: readonly SoftwareCatalogEntry[];
package/dist/software.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/software.ts
2
2
  import { readFileSync } from "node:fs";
3
+ var PINNED_BUN_VERSION = "1.3.14";
3
4
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
4
5
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
5
6
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
@@ -11,7 +12,8 @@ var SOFTWARE_CATALOG = [
11
12
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
12
13
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
13
14
  { 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
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
16
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
17
  ];
16
18
  var UBUNTU_2604_X64 = [
17
19
  {
@@ -38,6 +40,11 @@ var UBUNTU_2604_X64 = [
38
40
  requirement: { id: "ufw", version: "ubuntu-26.04" },
39
41
  check: "command -v ufw >/dev/null",
40
42
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
43
+ },
44
+ {
45
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
46
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
47
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
41
48
  }
42
49
  ];
43
50
  function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -62,7 +69,7 @@ function validateSoftwareRequirements(value, _options = {}) {
62
69
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
63
70
  throw new Error("software requirement contains an unknown field");
64
71
  }
65
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
72
+ if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
66
73
  throw new Error("software requirement coordinate is invalid");
67
74
  }
68
75
  const requirement = { id: row.id, version: row.version };
@@ -108,5 +115,7 @@ export {
108
115
  observeSoftwareHost,
109
116
  ensureSoftwareRequirements,
110
117
  SOFTWARE_CATALOG,
111
- OS_CATALOG
118
+ PINNED_BUN_VERSION,
119
+ OS_CATALOG,
120
+ BUN_INSTALLER_SHA256
112
121
  };
@@ -0,0 +1,72 @@
1
+ import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
+ import type { AgentOperationTelemetry } from './telemetry-runtime';
3
+ import { type GitHostResolver } from './git-egress';
4
+ export interface RemoteSshBootstrapClaim {
5
+ computeKey: string;
6
+ computeReference: string;
7
+ claimToken: string;
8
+ claimExpiresAtTs: number;
9
+ attempt: number;
10
+ realm: string;
11
+ projectKey: string;
12
+ environmentKey: string;
13
+ target: {
14
+ host: string;
15
+ port: number;
16
+ user: string;
17
+ hostKeySha256: string;
18
+ nodeHostname: string;
19
+ };
20
+ enrolmentToken: string;
21
+ }
22
+ export interface BootstrapCommandResult {
23
+ exitCode: number;
24
+ output: string;
25
+ }
26
+ export type BootstrapCommand = (argv: readonly string[], options?: {
27
+ stdin?: string;
28
+ secret?: boolean;
29
+ }) => Promise<BootstrapCommandResult>;
30
+ export interface SshBootstrapPullOptions {
31
+ apiUrl: string;
32
+ platformApiUrl: string;
33
+ nodeKey: string;
34
+ keys: NodeKeyPair;
35
+ sshKeyPath: string;
36
+ targetTelemetryEndpoint: string;
37
+ fzCliPath?: string;
38
+ fzAgentPath?: string;
39
+ exec?: BootstrapCommand;
40
+ fetch?: (input: URL, init: RequestInit) => Promise<Response>;
41
+ intervalMs?: number;
42
+ requestTimeoutMs?: number;
43
+ now?: () => number;
44
+ setTimer?: (callback: () => void, ms: number) => unknown;
45
+ clearTimer?: (handle: unknown) => void;
46
+ onEvent?: (event: string, detail?: unknown) => void;
47
+ telemetry?: AgentOperationTelemetry;
48
+ resolveHost?: GitHostResolver;
49
+ }
50
+ export type SshBootstrapResult = {
51
+ status: 'idle';
52
+ } | {
53
+ status: 'completed';
54
+ computeKey: string;
55
+ attempt: number;
56
+ } | {
57
+ status: 'failed';
58
+ computeKey: string;
59
+ attempt: number;
60
+ failureCode: string;
61
+ };
62
+ export declare class SshBootstrapError extends Error {
63
+ readonly code: string;
64
+ constructor(code: string, message?: string);
65
+ }
66
+ /** Execute one job without ever putting the enrolment token or SSH key in argv. */
67
+ export declare function executeSshBootstrap(claim: RemoteSshBootstrapClaim, options: Pick<SshBootstrapPullOptions, 'platformApiUrl' | 'sshKeyPath' | 'targetTelemetryEndpoint' | 'fzCliPath' | 'fzAgentPath' | 'exec' | 'resolveHost'>): Promise<void>;
68
+ export declare function pullSshBootstrapOnce(options: SshBootstrapPullOptions): Promise<SshBootstrapResult>;
69
+ export declare function startSshBootstrapPull(options: SshBootstrapPullOptions): {
70
+ stop(): Promise<void>;
71
+ readonly active: boolean;
72
+ };
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.35";
2
+ export declare const VERSION = "0.1.36";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -28,7 +28,7 @@
28
28
  "additionalProperties": false,
29
29
  "required": ["id", "version"],
30
30
  "properties": {
31
- "id": { "enum": ["bun", "nginx", "arangodb", "cloudflared", "ufw"] },
31
+ "id": { "enum": ["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"] },
32
32
  "version": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$" }
33
33
  }
34
34
  }