@forgezero/agent 0.1.87 → 0.1.88

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.
@@ -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.88";
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)
package/dist/bootstrap.js CHANGED
@@ -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.87";
1423
+ var VERSION = "0.1.88";
1424
1424
 
1425
1425
  // src/software.ts
1426
1426
  var PINNED_BUN_VERSION = "1.3.14";
package/dist/fz-agent.js CHANGED
@@ -9378,7 +9378,7 @@ async function writeAndCloseProcessInput(input, value) {
9378
9378
  }
9379
9379
 
9380
9380
  // src/version.ts
9381
- var VERSION2 = "0.1.87";
9381
+ var VERSION2 = "0.1.88";
9382
9382
 
9383
9383
  // src/ssh-bootstrap.ts
9384
9384
  class SshBootstrapError extends Error {
@@ -11987,7 +11987,7 @@ function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READ
11987
11987
  return;
11988
11988
  try {
11989
11989
  const value = JSON.parse(buffer.slice(0, newline));
11990
- finish(value.version === expected.version && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].includes(value.vault ?? ""));
11990
+ finish(value.version === expected.version && value.nodeKey === expected.nodeKey && value.bound === expected.bound && expected.vault.includes(value.vault));
11991
11991
  } catch {
11992
11992
  finish(false);
11993
11993
  }
@@ -12190,13 +12190,18 @@ var restartAgent = async (target2, run2) => {
12190
12190
  }
12191
12191
  };
12192
12192
  var candidateUnit = (target2) => target2 === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
12193
- var startCandidate = async (staged, target2, run2, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
12193
+ var startCandidate = async (staged, target2, expectedNodeKey, run2, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
12194
12194
  selectAgentCandidate(staged);
12195
12195
  if (!await runOk(run2, "/usr/bin/systemctl", ["restart", candidateUnit(target2)])) {
12196
12196
  clearAgentCandidate(dirname8(staged.currentLink));
12197
12197
  throw new Error(`systemd could not start ${candidateUnit(target2)}`);
12198
12198
  }
12199
- if (!await probe({ version: staged.version })) {
12199
+ if (!await probe({
12200
+ version: staged.version,
12201
+ nodeKey: expectedNodeKey,
12202
+ bound: true,
12203
+ vault: target2 === "compute" ? ["ready"] : ["unbound"]
12204
+ })) {
12200
12205
  await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12201
12206
  clearAgentCandidate(dirname8(staged.currentLink));
12202
12207
  throw new Error("the candidate Agent did not prove its identity and durable state");
@@ -12275,8 +12280,9 @@ async function activateAgentRelease(staged, options = {}) {
12275
12280
  writeUpdateState(journalPath, receiptPath, journal);
12276
12281
  let selectionAttempted = false;
12277
12282
  try {
12278
- if (!options.candidatePrepared)
12279
- await startCandidate(staged, target2, run2);
12283
+ if (!options.candidatePrepared) {
12284
+ throw new Error("direct activation requires a separately identity-bound candidate preparation");
12285
+ }
12280
12286
  if (target2 === "compute")
12281
12287
  switchAgentSocketRoute(paths.route, paths.candidate);
12282
12288
  selectionAttempted = true;
@@ -12466,6 +12472,9 @@ function startAgentUpdateHelper(options = {}) {
12466
12472
  }
12467
12473
  if (request.op !== "prepare")
12468
12474
  throw new Error("unknown update operation");
12475
+ if (typeof request.expectedNodeKey !== "string" || request.expectedNodeKey.length < 16 || Buffer.byteLength(request.expectedNodeKey, "utf8") > 512 || /[\0\r\n]/.test(request.expectedNodeKey)) {
12476
+ throw new Error("Agent update expected node identity is invalid");
12477
+ }
12469
12478
  if (busy)
12470
12479
  throw new Error("another Agent update or recovery is already active");
12471
12480
  const prior = readJournal(journalPath, releaseRoot);
@@ -12478,8 +12487,8 @@ function startAgentUpdateHelper(options = {}) {
12478
12487
  currentVersion: request.currentVersion,
12479
12488
  root: releaseRoot
12480
12489
  });
12481
- await startCandidate(staged, request.target, runCommand);
12482
- pending = { staged, target: request.target, attemptId };
12490
+ await startCandidate(staged, request.target, request.expectedNodeKey, runCommand);
12491
+ pending = { staged, target: request.target, attemptId, expectedNodeKey: request.expectedNodeKey };
12483
12492
  ownsBusy = false;
12484
12493
  setTimer(() => {
12485
12494
  if (pending?.attemptId !== attemptId)
@@ -12636,7 +12645,14 @@ async function heartbeatAgentOnce(options) {
12636
12645
  let candidatePrepared = false;
12637
12646
  let drained = false;
12638
12647
  try {
12639
- const update2 = options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate(phase === "prepare" ? { op: "prepare", target: options.updateTarget ?? "compute", release: next, currentVersion: current2, attemptId } : {
12648
+ const update2 = options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate(phase === "prepare" ? {
12649
+ op: "prepare",
12650
+ target: options.updateTarget ?? "compute",
12651
+ release: next,
12652
+ currentVersion: current2,
12653
+ expectedNodeKey: options.nodeKey,
12654
+ attemptId
12655
+ } : {
12640
12656
  op: phase,
12641
12657
  target: options.updateTarget ?? "compute",
12642
12658
  currentVersion: current2,
package/dist/fz.js CHANGED
@@ -4842,7 +4842,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4842
4842
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4843
4843
 
4844
4844
  // src/version.ts
4845
- var VERSION2 = "0.1.87";
4845
+ var VERSION2 = "0.1.88";
4846
4846
 
4847
4847
  // src/software.ts
4848
4848
  var PINNED_BUN_VERSION = "1.3.14";
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.87";
369
+ var VERSION = "0.1.88";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -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.87";
1423
+ var VERSION = "0.1.88";
1424
1424
 
1425
1425
  // src/software.ts
1426
1426
  var PINNED_BUN_VERSION = "1.3.14";
@@ -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)
@@ -2803,7 +2812,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2803
2812
  }
2804
2813
 
2805
2814
  // src/version.ts
2806
- var VERSION3 = "0.1.87";
2815
+ var VERSION3 = "0.1.88";
2807
2816
 
2808
2817
  // src/egress-policy.ts
2809
2818
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/provision.js CHANGED
@@ -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)
@@ -2803,7 +2812,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2803
2812
  }
2804
2813
 
2805
2814
  // src/version.ts
2806
- var VERSION3 = "0.1.87";
2815
+ var VERSION3 = "0.1.88";
2807
2816
 
2808
2817
  // src/egress-policy.ts
2809
2818
  import { realpathSync as realpathSync3 } from "node:fs";
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.87";
2
+ export declare const VERSION = "0.1.88";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.87",
3
+ "version": "0.1.88",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",