@forgezero/agent 0.1.87 → 0.1.89

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/README.md CHANGED
@@ -272,10 +272,11 @@ export const selectedCapability = DEFAULT_LIFECYCLE_HELPER_SOCKET;
272
272
  <a id="forgezero-agent-provision"></a>
273
273
  ## @forgezero/agent/provision
274
274
 
275
- The install plan as data, with no transport — so `fz` running locally and the platform running over SSH provision an identical machine. This entry exposes 34 named value exports and 11 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
275
+ The install plan as data, with no transport — so `fz` running locally and the platform running over SSH provision an identical machine. This entry exposes 38 named value exports and 11 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
276
276
 
277
277
  ```text
278
278
  import {
279
+ AGENT_CANDIDATE_UNIT_PATH,
279
280
  AGENT_EGRESS_UNIT_PATH,
280
281
  AGENT_SOCKET_PROXY_UNIT_PATH,
281
282
  AGENT_SOCKET_UNIT_PATH,
@@ -295,8 +296,11 @@ import {
295
296
  WARP_CONFIG_UNIT_PATH,
296
297
  WARP_SERVICE_DROP_IN_PATH,
297
298
  agentBackendSocketPath,
299
+ agentCandidateSocketPath,
300
+ agentCandidateUnit,
298
301
  agentEgressUnit,
299
302
  agentEnrolmentUnit,
303
+ agentRoutingSocketPath,
300
304
  agentSocketProxyUnit,
301
305
  agentSocketUnit,
302
306
  agentUnit,
@@ -333,10 +337,10 @@ This minimal executable use imports one concrete value from this exact entry poi
333
337
 
334
338
  ```text
335
339
  import {
336
- AGENT_EGRESS_UNIT_PATH,
340
+ AGENT_CANDIDATE_UNIT_PATH,
337
341
  } from '@forgezero/agent/provision';
338
342
 
339
- export const selectedCapability = AGENT_EGRESS_UNIT_PATH;
343
+ export const selectedCapability = AGENT_CANDIDATE_UNIT_PATH;
340
344
  ```
341
345
 
342
346
  <a id="forgezero-agent-platform-genesis"></a>
@@ -534,15 +538,18 @@ export const selectedCapability = heartbeatAgentOnce;
534
538
  <a id="forgezero-agent-agent-update"></a>
535
539
  ## @forgezero/agent/agent-update
536
540
 
537
- Verified Agent update planning against the platform release coordinate. This entry exposes 8 named value exports and 4 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
541
+ Verified Agent update planning against the platform release coordinate. This entry exposes 11 named value exports and 4 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
538
542
 
539
543
  ```text
540
544
  import {
545
+ DEFAULT_AGENT_CANDIDATE_LINK,
541
546
  DEFAULT_AGENT_RELEASE_ROOT,
542
547
  DEFAULT_AGENT_UPDATE_SOCKET,
543
548
  MAX_AGENT_TARBALL_BYTES,
549
+ clearAgentCandidate,
544
550
  compareVersions,
545
551
  restoreAgentRelease,
552
+ selectAgentCandidate,
546
553
  selectAgentRelease,
547
554
  stageAgentRelease,
548
555
  validateAgentRelease,
@@ -562,10 +569,10 @@ This minimal executable use imports one concrete value from this exact entry poi
562
569
 
563
570
  ```text
564
571
  import {
565
- DEFAULT_AGENT_RELEASE_ROOT,
572
+ DEFAULT_AGENT_CANDIDATE_LINK,
566
573
  } from '@forgezero/agent/agent-update';
567
574
 
568
- export const selectedCapability = DEFAULT_AGENT_RELEASE_ROOT;
575
+ export const selectedCapability = DEFAULT_AGENT_CANDIDATE_LINK;
569
576
  ```
570
577
 
571
578
  <a id="forgezero-agent-agent-update-helper"></a>
@@ -709,7 +716,7 @@ await readBootstrapBundle(built.bundlePath, built.manifestPath);
709
716
  <a id="forgezero-agent-operator-bootstrap"></a>
710
717
  ## @forgezero/agent/operator-bootstrap
711
718
 
712
- Pinned-host operator transport that stages owner-only handoffs and invokes the same typed platform bootstrap through the caller-approved SSH agent. This entry exposes 19 named value exports and 17 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
719
+ Pinned-host operator transport that stages owner-only handoffs and invokes the same typed platform bootstrap through the caller-approved SSH agent. This entry exposes 24 named value exports and 17 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
713
720
 
714
721
  ```text
715
722
  import {
@@ -718,6 +725,10 @@ import {
718
725
  applyOperatorPlatformFleetBootstrap,
719
726
  collectOperatorGuestHostKeys,
720
727
  createOperatorSshHop,
728
+ operatorMetalClusterBootstrapCode,
729
+ operatorPackagedAgentPath,
730
+ operatorPackagedFzCliPath,
731
+ operatorPackagedGitSshPath,
721
732
  operatorPlatformFleetCoordinates,
722
733
  planOperatorMetalBootstrap,
723
734
  planOperatorPlatformBootstrap,
@@ -726,6 +737,7 @@ import {
726
737
  readOperatorMetalBootstrapRequest,
727
738
  readOperatorPlatformBootstrapFleetRequest,
728
739
  readOperatorPlatformBootstrapRequest,
740
+ redactOperatorSecretDiagnostic,
729
741
  verifyOperatorPlatformFleet,
730
742
  withOperatorGuestTransport,
731
743
  writeOperatorGuestHostKeyEvidence,
@@ -800,7 +812,7 @@ console.log(plan.argv);
800
812
  <a id="forgezero-agent-capacity-calibration"></a>
801
813
  ## @forgezero/agent/capacity-calibration
802
814
 
803
- One-time target-local safe-concurrency calibration with private evidence. This entry exposes 3 named value exports and 4 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
815
+ One-time target-local safe-concurrency calibration with private evidence. This entry exposes 3 named value exports and 5 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
804
816
 
805
817
  ```text
806
818
  import {
@@ -813,6 +825,7 @@ import type {
813
825
  CapacityCalibration,
814
826
  CapacityCalibrationOptions,
815
827
  CapacityStage,
828
+ HostCapacitySample,
816
829
  ValidatedCapacityCalibrationOptions,
817
830
  } from '@forgezero/agent/capacity-calibration';
818
831
  ```
@@ -1560,11 +1573,12 @@ export const selectedCapability = MetalProvisionError;
1560
1573
  <a id="forgezero-agent-platform-bootstrap-runtime"></a>
1561
1574
  ## @forgezero/agent/platform-bootstrap-runtime
1562
1575
 
1563
- Pure platform bootstrap validation, render and resumable phase-state contracts. This entry exposes 10 named value exports and 14 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
1576
+ Pure platform bootstrap validation, render and resumable phase-state contracts. This entry exposes 11 named value exports and 14 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
1564
1577
 
1565
1578
  ```text
1566
1579
  import {
1567
1580
  activatePlatformRelease,
1581
+ assertReleaseLinksContained,
1568
1582
  planLocalOtlpProof,
1569
1583
  planPlatformActivation,
1570
1584
  platformApiCredentialSpecs,
@@ -6,8 +6,14 @@ export interface AgentCandidateReady {
6
6
  bound: boolean;
7
7
  vault: 'ready' | 'partial' | 'unavailable' | 'unbound';
8
8
  }
9
+ export interface AgentCandidateExpectation {
10
+ version: string;
11
+ nodeKey: string;
12
+ bound: boolean;
13
+ vault: readonly AgentCandidateReady['vault'][];
14
+ }
9
15
  /** New proxy connections resolve the new backend; established streams are untouched. */
10
16
  export declare function switchAgentSocketRoute(route: string, backend: string): void;
11
17
  /** Root-observable readiness appears only after identity, binding and Vault initialization. */
12
18
  export declare function startAgentCandidateReady(ready: AgentCandidateReady, socketPath?: string): Server;
13
- export declare function probeAgentCandidate(expected: Pick<AgentCandidateReady, 'version'>, socketPath?: string, timeoutMs?: number): Promise<boolean>;
19
+ export declare function probeAgentCandidate(expected: AgentCandidateExpectation, socketPath?: string, timeoutMs?: number): Promise<boolean>;
@@ -284,7 +284,7 @@ function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READ
284
284
  return;
285
285
  try {
286
286
  const value = JSON.parse(buffer.slice(0, newline));
287
- finish(value.version === expected.version && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].includes(value.vault ?? ""));
287
+ finish(value.version === expected.version && value.nodeKey === expected.nodeKey && value.bound === expected.bound && expected.vault.includes(value.vault));
288
288
  } catch {
289
289
  finish(false);
290
290
  }
@@ -487,13 +487,18 @@ var restartAgent = async (target, run) => {
487
487
  }
488
488
  };
489
489
  var candidateUnit = (target) => target === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
490
- var startCandidate = async (staged, target, run, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
490
+ var startCandidate = async (staged, target, expectedNodeKey, run, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
491
491
  selectAgentCandidate(staged);
492
492
  if (!await runOk(run, "/usr/bin/systemctl", ["restart", candidateUnit(target)])) {
493
493
  clearAgentCandidate(dirname3(staged.currentLink));
494
494
  throw new Error(`systemd could not start ${candidateUnit(target)}`);
495
495
  }
496
- if (!await probe({ version: staged.version })) {
496
+ if (!await probe({
497
+ version: staged.version,
498
+ nodeKey: expectedNodeKey,
499
+ bound: true,
500
+ vault: target === "compute" ? ["ready"] : ["unbound"]
501
+ })) {
497
502
  await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
498
503
  clearAgentCandidate(dirname3(staged.currentLink));
499
504
  throw new Error("the candidate Agent did not prove its identity and durable state");
@@ -572,8 +577,9 @@ async function activateAgentRelease(staged, options = {}) {
572
577
  writeUpdateState(journalPath, receiptPath, journal);
573
578
  let selectionAttempted = false;
574
579
  try {
575
- if (!options.candidatePrepared)
576
- await startCandidate(staged, target, run);
580
+ if (!options.candidatePrepared) {
581
+ throw new Error("direct activation requires a separately identity-bound candidate preparation");
582
+ }
577
583
  if (target === "compute")
578
584
  switchAgentSocketRoute(paths.route, paths.candidate);
579
585
  selectionAttempted = true;
@@ -763,6 +769,9 @@ function startAgentUpdateHelper(options = {}) {
763
769
  }
764
770
  if (request.op !== "prepare")
765
771
  throw new Error("unknown update operation");
772
+ if (typeof request.expectedNodeKey !== "string" || request.expectedNodeKey.length < 16 || Buffer.byteLength(request.expectedNodeKey, "utf8") > 512 || /[\0\r\n]/.test(request.expectedNodeKey)) {
773
+ throw new Error("Agent update expected node identity is invalid");
774
+ }
766
775
  if (busy)
767
776
  throw new Error("another Agent update or recovery is already active");
768
777
  const prior = readJournal(journalPath, releaseRoot);
@@ -775,8 +784,8 @@ function startAgentUpdateHelper(options = {}) {
775
784
  currentVersion: request.currentVersion,
776
785
  root: releaseRoot
777
786
  });
778
- await startCandidate(staged, request.target, runCommand);
779
- pending = { staged, target: request.target, attemptId };
787
+ await startCandidate(staged, request.target, request.expectedNodeKey, runCommand);
788
+ pending = { staged, target: request.target, attemptId, expectedNodeKey: request.expectedNodeKey };
780
789
  ownsBusy = false;
781
790
  setTimer(() => {
782
791
  if (pending?.attemptId !== attemptId)
@@ -908,7 +917,7 @@ async function postSignedNode(options, path, body) {
908
917
  }
909
918
 
910
919
  // src/version.ts
911
- var VERSION3 = "0.1.87";
920
+ var VERSION3 = "0.1.89";
912
921
 
913
922
  // src/agent-heartbeat.ts
914
923
  function readAgentHostMetrics() {
@@ -1009,7 +1018,14 @@ async function heartbeatAgentOnce(options) {
1009
1018
  let candidatePrepared = false;
1010
1019
  let drained = false;
1011
1020
  try {
1012
- const update2 = options.applyUpdate ?? ((phase, next, current, attemptId) => requestAgentUpdate(phase === "prepare" ? { op: "prepare", target: options.updateTarget ?? "compute", release: next, currentVersion: current, attemptId } : {
1021
+ const update2 = options.applyUpdate ?? ((phase, next, current, attemptId) => requestAgentUpdate(phase === "prepare" ? {
1022
+ op: "prepare",
1023
+ target: options.updateTarget ?? "compute",
1024
+ release: next,
1025
+ currentVersion: current,
1026
+ expectedNodeKey: options.nodeKey,
1027
+ attemptId
1028
+ } : {
1013
1029
  op: phase,
1014
1030
  target: options.updateTarget ?? "compute",
1015
1031
  currentVersion: current,
@@ -11,6 +11,7 @@ export type AgentUpdateRequest = {
11
11
  target: 'compute' | 'metal';
12
12
  attemptId?: string;
13
13
  currentVersion: string;
14
+ expectedNodeKey: string;
14
15
  release: AgentRelease;
15
16
  } | {
16
17
  op: 'commit' | 'abort';
@@ -284,7 +284,7 @@ function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READ
284
284
  return;
285
285
  try {
286
286
  const value = JSON.parse(buffer.slice(0, newline));
287
- finish(value.version === expected.version && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].includes(value.vault ?? ""));
287
+ finish(value.version === expected.version && value.nodeKey === expected.nodeKey && value.bound === expected.bound && expected.vault.includes(value.vault));
288
288
  } catch {
289
289
  finish(false);
290
290
  }
@@ -487,13 +487,18 @@ var restartAgent = async (target, run) => {
487
487
  }
488
488
  };
489
489
  var candidateUnit = (target) => target === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
490
- var startCandidate = async (staged, target, run, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
490
+ var startCandidate = async (staged, target, expectedNodeKey, run, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
491
491
  selectAgentCandidate(staged);
492
492
  if (!await runOk(run, "/usr/bin/systemctl", ["restart", candidateUnit(target)])) {
493
493
  clearAgentCandidate(dirname3(staged.currentLink));
494
494
  throw new Error(`systemd could not start ${candidateUnit(target)}`);
495
495
  }
496
- if (!await probe({ version: staged.version })) {
496
+ if (!await probe({
497
+ version: staged.version,
498
+ nodeKey: expectedNodeKey,
499
+ bound: true,
500
+ vault: target === "compute" ? ["ready"] : ["unbound"]
501
+ })) {
497
502
  await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
498
503
  clearAgentCandidate(dirname3(staged.currentLink));
499
504
  throw new Error("the candidate Agent did not prove its identity and durable state");
@@ -572,8 +577,9 @@ async function activateAgentRelease(staged, options = {}) {
572
577
  writeUpdateState(journalPath, receiptPath, journal);
573
578
  let selectionAttempted = false;
574
579
  try {
575
- if (!options.candidatePrepared)
576
- await startCandidate(staged, target, run);
580
+ if (!options.candidatePrepared) {
581
+ throw new Error("direct activation requires a separately identity-bound candidate preparation");
582
+ }
577
583
  if (target === "compute")
578
584
  switchAgentSocketRoute(paths.route, paths.candidate);
579
585
  selectionAttempted = true;
@@ -763,6 +769,9 @@ function startAgentUpdateHelper(options = {}) {
763
769
  }
764
770
  if (request.op !== "prepare")
765
771
  throw new Error("unknown update operation");
772
+ if (typeof request.expectedNodeKey !== "string" || request.expectedNodeKey.length < 16 || Buffer.byteLength(request.expectedNodeKey, "utf8") > 512 || /[\0\r\n]/.test(request.expectedNodeKey)) {
773
+ throw new Error("Agent update expected node identity is invalid");
774
+ }
766
775
  if (busy)
767
776
  throw new Error("another Agent update or recovery is already active");
768
777
  const prior = readJournal(journalPath, releaseRoot);
@@ -775,8 +784,8 @@ function startAgentUpdateHelper(options = {}) {
775
784
  currentVersion: request.currentVersion,
776
785
  root: releaseRoot
777
786
  });
778
- await startCandidate(staged, request.target, runCommand);
779
- pending = { staged, target: request.target, attemptId };
787
+ await startCandidate(staged, request.target, request.expectedNodeKey, runCommand);
788
+ pending = { staged, target: request.target, attemptId, expectedNodeKey: request.expectedNodeKey };
780
789
  ownsBusy = false;
781
790
  setTimer(() => {
782
791
  if (pending?.attemptId !== attemptId)
@@ -1,5 +1,5 @@
1
1
  import { type ProvisionPlan } from './cli/agent-install';
2
- import type { Capabilities } from './provision';
2
+ import { type Capabilities } from './provision';
3
3
  import { type SoftwareCommandResult, type SoftwareRequirement } from './software';
4
4
  import { type PlatformSharedEnvironment } from './platform-bootstrap-runtime';
5
5
  import { type BootstrapBundleManifest } from './bootstrap-bundle';
@@ -191,6 +191,13 @@ export declare const BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
191
191
  export declare function validateBootstrapConfig(value: BootstrapConfig): BootstrapConfig;
192
192
  export declare function planBootstrap(input: BootstrapConfig, initialized?: boolean): BootstrapPlan;
193
193
  export declare function bootstrapIdentityDigest(config: BootstrapConfig): string;
194
+ export declare const legacyBootstrapIdentityDigest: (config: BootstrapConfig) => string;
195
+ /** Exact prior-release identities accepted only while generation one is still unenrolled. */
196
+ export declare function interruptedPlatformReleaseIdentityDigests(config: PlatformBootstrapConfig, releaseEvidence: {
197
+ revision: string;
198
+ sha256: string;
199
+ branch: string;
200
+ }, bootstrapManifest?: BootstrapBundleManifest): readonly string[];
194
201
  export declare function bootstrapStatus(host?: BootstrapHost): Promise<BootstrapStatus>;
195
202
  export declare function applyBootstrap(input: BootstrapConfig, host?: BootstrapHost, secrets?: BootstrapSecrets, dependencies?: {
196
203
  fetcher?: typeof fetch;