@forgezero/agent 0.1.38 → 0.1.39

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
@@ -15,7 +15,10 @@ fz --help
15
15
 
16
16
  The one public package installs both commands. Keeping bootstrap and the daemon
17
17
  in one version prevents a newly installed `fz` from provisioning a different
18
- agent protocol.
18
+ agent protocol. Both executable artifacts bundle their ForgeZero runtime
19
+ dependencies, so the constrained SSH bootstrap can transfer the reviewed pair
20
+ to a new host without relying on a checkout, `node_modules`, or a second remote
21
+ package-resolution step. Library subpath exports remain modular packages.
19
22
 
20
23
  The operator command starts browser-first platform genesis, inspects status,
21
24
  wraps processes, and installs the daemon. Genesis opens the one-use founder
@@ -43,13 +46,33 @@ script and no second implementation for tenant or platform machines:
43
46
 
44
47
  ```bash
45
48
  fz bootstrap platform # interactive plan, no mutation
46
- sudo fz bootstrap platform --apply # initial DB+API or API-only host
49
+ sudo fz bootstrap platform prepare --bootstrap-config ./platform.json --apply
50
+ # Register the printed read-only deploy key. For genesis, run --apply on all
51
+ # three Agency members concurrently; later agency:none joiners run alone.
52
+ sudo fz bootstrap platform --bootstrap-config ./platform.json --apply
47
53
  sudo fz bootstrap tenant --bootstrap-config ./tenant.json --apply
48
54
  sudo fz bootstrap metal --bootstrap-config ./metal.json --apply
49
55
  sudo fz bootstrap status
50
56
  sudo fz bootstrap repair --bootstrap-config ./original.json --apply
51
57
  ```
52
58
 
59
+ Platform database participation is explicit in the private JSON configuration.
60
+ The three genesis nodes use `database.agency: "member"`; later DB+API joiners
61
+ normally use `database.agency: "none"`, which starts a Coordinator and DBServer
62
+ without adding a fourth Agency member. Enrolment is likewise explicit:
63
+ `enrolment.source: "genesis-derived"` only for the offline genesis fleet, or
64
+ `"api-token"` with `tokenFile` for every API-authorized later node. No behavior
65
+ is inferred from a hostname or numeric suffix.
66
+
67
+ Repair is not a second installation path: the CLI detects the installed
68
+ platform/tenant or metal state and dispatches to that profile's validator. It
69
+ fails closed if both state formats are present or neither is present. A private
70
+ intent journal binds an interrupted first apply, and later repair refuses a
71
+ config whose immutable host identity differs from the installed profile. Status checks the persisted
72
+ identity record, both Agent sockets, profile-owned systemd units and, for a
73
+ platform host, the active API health endpoint, nginx configuration and live
74
+ Coordinator-mode evidence.
75
+
53
76
  Platform bootstrap supports an elastic Community ArangoDB 3.11.14 fleet. The
54
77
  first three database-capable computes establish the writable cluster; later
55
78
  database joiners and API-only computes use the same command with their typed
@@ -73,6 +96,8 @@ enters the API or Agent service environment:
73
96
  chmod 600 cloudflare-bootstrap.json cloudflare-management.token
74
97
  fz bootstrap platform cloudflare --bootstrap-config ./cloudflare-bootstrap.json
75
98
  fz bootstrap platform cloudflare --bootstrap-config ./cloudflare-bootstrap.json --apply
99
+ # After every host is bootstrapped and publishing healthy KV state:
100
+ fz bootstrap platform cloudflare verify --bootstrap-config ./cloudflare-handoff.json
76
101
  ```
77
102
 
78
103
  The reviewed JSON has `format: 1`, kind
@@ -83,8 +108,16 @@ and Access application name. Relative token, checkpoint and Worker project
83
108
  paths resolve beside the config file. Plan is offline. Apply creates/reuses one
84
109
  shared KV namespace, Worker and Access service token plus one Tunnel, Access
85
110
  application, ingress rule and DNS record per node. The command prints only
86
- resource IDs; connector, Access and requested runtime-token values stay in the
87
- atomically updated owner-only checkpoint for direct systemd-credential handoff.
111
+ resource IDs and per-node handoff paths. The complete owner-only checkpoint
112
+ stays on the operator laptop; `<checkpoint>.hosts/<node>.json` contains only
113
+ that node's connector and required runtime capabilities and is the file copied
114
+ to the host for `fz bootstrap platform`. Management tokens are never persisted.
115
+ Each host pins cloudflared diagnostics to loopback and bootstrap/status requires
116
+ the expected Tunnel UUID, a connector UUID, and all four documented edge
117
+ connections—not merely an active process. The final `verify` is read-only: it
118
+ uses the checkpoint's Access service credential only on the operator laptop to
119
+ probe every origin, then probes every stable Worker domain without privileged
120
+ headers. Its printed evidence contains hostnames and HTTP status codes only.
88
121
 
89
122
  ## What changes for an application
90
123
 
@@ -11,6 +11,11 @@ export interface AgentObservation {
11
11
  architecture: string;
12
12
  mode: 'attested' | 'enrolled';
13
13
  update?: AgentUpdateReceipt;
14
+ deploymentIntake?: {
15
+ generation: number;
16
+ state: 'running' | 'paused';
17
+ appliedAtTs: number;
18
+ };
14
19
  }
15
20
  export interface AgentHeartbeatResponse {
16
21
  ok: true;
@@ -22,6 +27,10 @@ export interface AgentHeartbeatResponse {
22
27
  leaseExpiresAtTs: number;
23
28
  release: AgentRelease;
24
29
  };
30
+ deploymentIntake?: {
31
+ generation: number;
32
+ state: 'running' | 'paused';
33
+ };
25
34
  }
26
35
  export interface AgentHeartbeatOptions {
27
36
  apiUrl: string;
@@ -33,6 +42,10 @@ export interface AgentHeartbeatOptions {
33
42
  fetch?: (input: URL, init: RequestInit) => Promise<Response>;
34
43
  requestTimeoutMs?: number;
35
44
  observation?: () => AgentObservation;
45
+ /** Last applied server-owned deployment-intake generation, reported on the next heartbeat. */
46
+ deploymentIntakeObservation?: () => AgentObservation['deploymentIntake'];
47
+ /** Apply the authenticated desired state before any new deployment claim is allowed. */
48
+ applyDeploymentIntake?: (directive: NonNullable<AgentHeartbeatResponse['deploymentIntake']>) => Promise<void> | void;
36
49
  receiptPath?: string;
37
50
  now?: () => number;
38
51
  /** Stop new lifecycle/deploy claims and await current jobs before replacement. */
@@ -744,7 +744,7 @@ async function postSignedNode(options, path, body) {
744
744
  }
745
745
 
746
746
  // src/version.ts
747
- var VERSION3 = "0.1.38";
747
+ var VERSION3 = "0.1.39";
748
748
 
749
749
  // src/agent-heartbeat.ts
750
750
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
@@ -770,7 +770,15 @@ async function heartbeatAgentOnce(options) {
770
770
  const update = observation.update ?? readAgentUpdateReceipt(options.receiptPath ?? AGENT_UPDATE_RECEIPT);
771
771
  if (update)
772
772
  observation = { ...observation, update };
773
+ const deploymentIntake = options.deploymentIntakeObservation?.();
774
+ if (deploymentIntake)
775
+ observation = { ...observation, deploymentIntake };
773
776
  const response = await postSignedNode(options, "v1/node/heartbeat", observation);
777
+ if (response.deploymentIntake !== undefined) {
778
+ if (!Number.isSafeInteger(response.deploymentIntake.generation) || response.deploymentIntake.generation < 0 || response.deploymentIntake.state !== "running" && response.deploymentIntake.state !== "paused")
779
+ throw new Error("agent heartbeat deployment-intake directive is invalid");
780
+ await options.applyDeploymentIntake?.(response.deploymentIntake);
781
+ }
774
782
  const desired = response.desiredAgentUpdate ?? (response.desiredAgentRelease ? {
775
783
  attemptId: `legacy:${response.desiredAgentRelease.version}`,
776
784
  leaseExpiresAtTs: Number.MAX_SAFE_INTEGER,
@@ -4,6 +4,8 @@ import { type PlatformSharedEnvironment } from './platform-bootstrap-runtime';
4
4
  export declare const PLATFORM_BOOTSTRAP_PROFILES: readonly ["platform-db-api", "platform-api"];
5
5
  export type PlatformBootstrapProfile = (typeof PLATFORM_BOOTSTRAP_PROFILES)[number];
6
6
  export type DatabaseBootstrapRole = 'master' | 'joiner' | 'none';
7
+ export type DatabaseAgencyParticipation = 'member' | 'none';
8
+ export type PlatformEnrolmentSource = 'genesis-derived' | 'api-token';
7
9
  export type BootstrapEnvironment = 'production' | 'development';
8
10
  export interface PlatformBootstrapConfig {
9
11
  kind: 'platform';
@@ -19,6 +21,8 @@ export interface PlatformBootstrapConfig {
19
21
  telemetryEndpoint: string;
20
22
  database: {
21
23
  role: DatabaseBootstrapRole;
24
+ /** Genesis master/joiners are Agency members; elastic joiners normally use `none`. */
25
+ agency: DatabaseAgencyParticipation;
22
26
  /** Cluster mode must remain default. Bootstrap verifies it and never mutates it. */
23
27
  serverMode: 'default';
24
28
  address?: string;
@@ -27,8 +31,12 @@ export interface PlatformBootstrapConfig {
27
31
  /** Owner/root-only file containing the shared 64-hex bootstrap code. */
28
32
  bootstrapSecretFile: string;
29
33
  };
30
- /** Required for every post-genesis compute; initial n1..n3 derive their one-use capability. */
31
- platformEnrolTokenFile?: string;
34
+ /** Explicit authority source; never inferred from a node number. */
35
+ enrolment: {
36
+ source: PlatformEnrolmentSource;
37
+ /** Required only for an API-issued post-genesis capability. */
38
+ tokenFile?: string;
39
+ };
32
40
  runtime: {
33
41
  environment: PlatformSharedEnvironment;
34
42
  serviceUser: string;
@@ -49,9 +57,9 @@ export interface PlatformBootstrapConfig {
49
57
  };
50
58
  /** Install the connector binary only. Resource creation belongs to attended Cloudflare bootstrap. */
51
59
  installCloudflared?: boolean;
52
- /** Completed attended resource checkpoint; root seals only this node's runtime capabilities. */
60
+ /** Node-specific attended handoff; a host must never receive the fleet checkpoint. */
53
61
  cloudflareHandoff?: {
54
- checkpointFile: string;
62
+ handoffFile: string;
55
63
  nodeName: string;
56
64
  };
57
65
  }
@@ -97,6 +105,13 @@ export interface BootstrapResult {
97
105
  inviteUrl: string;
98
106
  };
99
107
  }
108
+ export interface PlatformBootstrapPreparation {
109
+ kind: 'platform';
110
+ prepared: true;
111
+ identityDigest: string;
112
+ gitPublicKey: string;
113
+ next: 'register this read-only deploy key, then run --apply concurrently on all three genesis Agency members';
114
+ }
100
115
  export interface BootstrapStatus {
101
116
  initialized: boolean;
102
117
  kind?: BootstrapConfig['kind'];
@@ -104,6 +119,12 @@ export interface BootstrapStatus {
104
119
  services: Record<string, boolean>;
105
120
  problems: string[];
106
121
  }
122
+ export type InstalledBootstrapKind = 'compute' | 'metal';
123
+ /** One host has one bootstrap authority. Dual state is never resolved by precedence. */
124
+ export declare function resolveInstalledBootstrapKind(states: {
125
+ compute: boolean;
126
+ metal: boolean;
127
+ }): InstalledBootstrapKind | undefined;
107
128
  export interface BootstrapHost {
108
129
  uid(): number;
109
130
  exists(path: string): boolean;
@@ -122,11 +143,22 @@ export interface BootstrapHost {
122
143
  exec(argv: readonly string[], options?: {
123
144
  stdin?: string;
124
145
  }): Promise<SoftwareCommandResult>;
146
+ sleep?(milliseconds: number): Promise<void>;
125
147
  ensureSoftware(requirements: readonly SoftwareRequirement[]): Promise<unknown>;
126
148
  installAgent(config: BootstrapConfig, enrolTokenSourcePath?: string): Promise<ProvisionPlan>;
127
149
  }
150
+ export declare const BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
128
151
  export declare function validateBootstrapConfig(value: BootstrapConfig): BootstrapConfig;
129
152
  export declare function planBootstrap(input: BootstrapConfig, initialized?: boolean): BootstrapPlan;
153
+ export declare function bootstrapIdentityDigest(config: BootstrapConfig): string;
154
+ /**
155
+ * Install only the common Agent boundary and generate its machine-local Git
156
+ * identity. Private repositories cannot be deployed until the owner registers
157
+ * this public key, so pretending one monolithic first command can do both is a
158
+ * sequencing bug. The bound intent makes the later apply resumable and refuses
159
+ * a different config after this first privileged mutation.
160
+ */
161
+ export declare function preparePlatformBootstrap(input: PlatformBootstrapConfig, host?: BootstrapHost): Promise<PlatformBootstrapPreparation>;
130
162
  export declare function bootstrapStatus(host?: BootstrapHost): Promise<BootstrapStatus>;
131
163
  export declare function applyBootstrap(input: BootstrapConfig, host?: BootstrapHost): Promise<BootstrapResult>;
132
164
  export declare function readBootstrapConfig(path: string): BootstrapConfig;