@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 +37 -4
- package/dist/agent-heartbeat.d.ts +13 -0
- package/dist/agent-heartbeat.js +9 -1
- package/dist/bootstrap.d.ts +36 -4
- package/dist/bootstrap.js +516 -59
- package/dist/cli/cloudflare-bootstrap.d.ts +5 -1
- package/dist/cloudflare-bootstrap.d.ts +31 -2
- package/dist/cloudflare-bootstrap.js +168 -27
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/fz-agent.js +6217 -20
- package/dist/fz.js +10766 -241
- package/dist/metal-bootstrap.d.ts +1 -0
- package/dist/metal-bootstrap.js +5 -3
- package/dist/platform-bootstrap-runtime.d.ts +0 -28
- package/dist/platform-bootstrap-runtime.js +1 -71
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
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 --
|
|
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
|
|
87
|
-
|
|
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. */
|
package/dist/agent-heartbeat.js
CHANGED
|
@@ -744,7 +744,7 @@ async function postSignedNode(options, path, body) {
|
|
|
744
744
|
}
|
|
745
745
|
|
|
746
746
|
// src/version.ts
|
|
747
|
-
var VERSION3 = "0.1.
|
|
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,
|
package/dist/bootstrap.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
31
|
-
|
|
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
|
-
/**
|
|
60
|
+
/** Node-specific attended handoff; a host must never receive the fleet checkpoint. */
|
|
53
61
|
cloudflareHandoff?: {
|
|
54
|
-
|
|
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;
|