@forgezero/agent 0.1.22 → 0.1.23
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 +24 -0
- package/dist/cli/agent-install.d.ts +6 -0
- package/dist/fz-agent.js +471 -218
- package/dist/fz.js +191 -17
- package/dist/index.d.ts +5 -0
- package/dist/lifecycle-helper.d.ts +36 -0
- package/dist/lifecycle-helper.js +216 -0
- package/dist/metal-helper-socket.js +8 -532
- package/dist/metal-provision.d.ts +1 -6
- package/dist/metal-provision.js +8 -536
- package/dist/migration-pull.d.ts +55 -0
- package/dist/migration-pull.js +185 -0
- package/dist/provision.d.ts +17 -0
- package/dist/provision.js +189 -15
- package/dist/version.d.ts +1 -1
- package/dist/warp-config.d.ts +20 -0
- package/package.json +10 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// src/signed-node-http.ts
|
|
2
|
+
import {
|
|
3
|
+
encodeSignatureHeader,
|
|
4
|
+
generateResponseRecipient,
|
|
5
|
+
openResponse,
|
|
6
|
+
RESPONSE_KEY_HEADER,
|
|
7
|
+
signRequest
|
|
8
|
+
} from "@forgezero/runtime/identity";
|
|
9
|
+
|
|
10
|
+
class SignedNodeHttpError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(status, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.name = "SignedNodeHttpError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function postSignedNode(options, path, body) {
|
|
19
|
+
const url = new URL(options.apiUrl);
|
|
20
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
21
|
+
url.search = "";
|
|
22
|
+
url.hash = "";
|
|
23
|
+
const raw = JSON.stringify(body);
|
|
24
|
+
const recipient = generateResponseRecipient();
|
|
25
|
+
const envelope = signRequest(options.keys, options.nodeKey, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
path: url.pathname,
|
|
28
|
+
query: "",
|
|
29
|
+
body: raw,
|
|
30
|
+
responseKey: recipient.publicKey
|
|
31
|
+
});
|
|
32
|
+
const signature = encodeSignatureHeader(envelope);
|
|
33
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": "application/json",
|
|
37
|
+
"x-fz-node": options.nodeKey,
|
|
38
|
+
"x-fz-signature": signature,
|
|
39
|
+
[RESPONSE_KEY_HEADER]: recipient.publicKey
|
|
40
|
+
},
|
|
41
|
+
body: raw,
|
|
42
|
+
signal: AbortSignal.timeout(options.requestTimeoutMs ?? 15000)
|
|
43
|
+
});
|
|
44
|
+
const payload = await response.json().catch(() => null);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const failure = payload;
|
|
47
|
+
const reason = failure ? failure.error?.message ?? failure.message : undefined;
|
|
48
|
+
throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await openResponse(recipient.secretKey, signature, payload);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new SignedNodeHttpError(502, "The node response was not sealed to this request.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/migration-pull.ts
|
|
58
|
+
class MigrationClaimLostError extends Error {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "MigrationClaimLostError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var post = (options, operation, body) => postSignedNode(options, `v1/node/migrations/${operation}`, body);
|
|
65
|
+
async function complete(options, body) {
|
|
66
|
+
const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
|
|
67
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
68
|
+
let last;
|
|
69
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
70
|
+
try {
|
|
71
|
+
await post(options, "complete", body);
|
|
72
|
+
return;
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
last = cause;
|
|
75
|
+
if (cause instanceof SignedNodeHttpError && cause.status < 500)
|
|
76
|
+
throw cause;
|
|
77
|
+
if (attempt < attempts)
|
|
78
|
+
await sleep(Math.min(2000, 250 * 2 ** (attempt - 1)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw last;
|
|
82
|
+
}
|
|
83
|
+
async function pullMigrationOnce(options) {
|
|
84
|
+
const response = await post(options, "claim", {});
|
|
85
|
+
if (!response.claim)
|
|
86
|
+
return { status: "idle" };
|
|
87
|
+
const claim = response.claim;
|
|
88
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
89
|
+
const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
90
|
+
const now = options.now ?? Date.now;
|
|
91
|
+
let expires = claim.claimExpiresAtTs;
|
|
92
|
+
let stopped = false;
|
|
93
|
+
let timer;
|
|
94
|
+
let renewal = null;
|
|
95
|
+
let lost = null;
|
|
96
|
+
const schedule = (override) => {
|
|
97
|
+
if (stopped)
|
|
98
|
+
return;
|
|
99
|
+
const delay = override ?? Math.max(1000, Math.min(5 * 60000, Math.floor(Math.max(0, expires - now()) / 3)));
|
|
100
|
+
timer = setTimer(() => {
|
|
101
|
+
if (stopped || renewal)
|
|
102
|
+
return;
|
|
103
|
+
let retry;
|
|
104
|
+
renewal = post(options, "renew", {
|
|
105
|
+
migrationKey: claim.migrationKey,
|
|
106
|
+
claimToken: claim.claimToken
|
|
107
|
+
}).then((value) => {
|
|
108
|
+
expires = value.claimExpiresAtTs;
|
|
109
|
+
}).catch((cause) => {
|
|
110
|
+
if (cause instanceof SignedNodeHttpError && cause.status < 500) {
|
|
111
|
+
lost = new MigrationClaimLostError(cause.message);
|
|
112
|
+
stopped = true;
|
|
113
|
+
} else
|
|
114
|
+
retry = Math.max(1000, options.renewRetryMs ?? 5000);
|
|
115
|
+
}).finally(() => {
|
|
116
|
+
renewal = null;
|
|
117
|
+
if (!stopped)
|
|
118
|
+
schedule(retry);
|
|
119
|
+
});
|
|
120
|
+
}, delay);
|
|
121
|
+
};
|
|
122
|
+
schedule();
|
|
123
|
+
let evidence;
|
|
124
|
+
try {
|
|
125
|
+
evidence = await options.run(claim);
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
stopped = true;
|
|
128
|
+
clearTimer(timer);
|
|
129
|
+
await renewal;
|
|
130
|
+
if (lost)
|
|
131
|
+
throw lost;
|
|
132
|
+
const reason = (cause instanceof Error ? cause.message : String(cause)).slice(0, 2000);
|
|
133
|
+
await complete(options, {
|
|
134
|
+
migrationKey: claim.migrationKey,
|
|
135
|
+
claimToken: claim.claimToken,
|
|
136
|
+
ok: false,
|
|
137
|
+
detail: reason
|
|
138
|
+
});
|
|
139
|
+
return { status: "failed", claim, reason };
|
|
140
|
+
}
|
|
141
|
+
stopped = true;
|
|
142
|
+
clearTimer(timer);
|
|
143
|
+
await renewal;
|
|
144
|
+
if (lost)
|
|
145
|
+
throw lost;
|
|
146
|
+
await complete(options, {
|
|
147
|
+
migrationKey: claim.migrationKey,
|
|
148
|
+
claimToken: claim.claimToken,
|
|
149
|
+
ok: true,
|
|
150
|
+
evidence
|
|
151
|
+
});
|
|
152
|
+
return { status: "completed", claim, evidence };
|
|
153
|
+
}
|
|
154
|
+
function startMigrationPull(options) {
|
|
155
|
+
const interval = Math.max(1000, options.intervalMs ?? 5000);
|
|
156
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
157
|
+
const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
158
|
+
let stopped = false;
|
|
159
|
+
let timer;
|
|
160
|
+
let active = null;
|
|
161
|
+
const tick = () => {
|
|
162
|
+
if (stopped || active)
|
|
163
|
+
return;
|
|
164
|
+
active = pullMigrationOnce(options).then((result) => options.onEvent?.(result.status, result)).catch((cause) => options.onEvent?.("poll-failed", cause)).finally(() => {
|
|
165
|
+
active = null;
|
|
166
|
+
if (!stopped)
|
|
167
|
+
timer = setTimer(tick, interval);
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
tick();
|
|
171
|
+
return {
|
|
172
|
+
async stop() {
|
|
173
|
+
stopped = true;
|
|
174
|
+
clearTimer(timer);
|
|
175
|
+
await active;
|
|
176
|
+
},
|
|
177
|
+
get active() {
|
|
178
|
+
return !stopped;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
startMigrationPull,
|
|
184
|
+
pullMigrationOnce
|
|
185
|
+
};
|
package/dist/provision.d.ts
CHANGED
|
@@ -92,6 +92,15 @@ export interface UnitOptions {
|
|
|
92
92
|
deploymentCredentials?: Record<string, string>;
|
|
93
93
|
/** Enable authenticated outbound deployment claims after enrolment. */
|
|
94
94
|
pullDeployments?: boolean;
|
|
95
|
+
/** Enable PQ-authenticated lifecycle claims through a constrained root helper. */
|
|
96
|
+
pullMigrations?: boolean;
|
|
97
|
+
/** Root-owned declarative lifecycle profile; never supplied by a migration claim. */
|
|
98
|
+
lifecycleProfilePath?: string;
|
|
99
|
+
lifecycleHelperSocketPath?: string;
|
|
100
|
+
/** Optional cross-network overlay. All three values are required together. */
|
|
101
|
+
warpOrganization?: string;
|
|
102
|
+
warpClientIdCredentialPath?: string;
|
|
103
|
+
warpClientSecretCredentialPath?: string;
|
|
95
104
|
/** Where `fz-agent` ended up. `bun add -g` puts it on PATH. */
|
|
96
105
|
binPath?: string;
|
|
97
106
|
/** Package-owned binary copied to binPath before hardened units start. */
|
|
@@ -109,9 +118,17 @@ export interface UnitOptions {
|
|
|
109
118
|
export declare const DEPLOYMENT_RUNNER_USER = "forgezero-runner";
|
|
110
119
|
export declare const DEPLOYMENT_GROUP = "forgezero-deploy";
|
|
111
120
|
export declare const VAULT_GROUP = "forgezero-vault";
|
|
121
|
+
export declare const LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
112
122
|
export declare const DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
113
123
|
export declare const DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
114
124
|
export declare const ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
125
|
+
export declare const LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
126
|
+
export declare const LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
127
|
+
export declare const WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
|
|
128
|
+
export declare const WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
|
|
129
|
+
export declare function warpConfigUnit(options: Pick<UnitOptions, 'binPath' | 'warpOrganization' | 'warpClientIdCredentialPath' | 'warpClientSecretCredentialPath'>): string;
|
|
130
|
+
export declare function warpServiceDropIn(): string;
|
|
131
|
+
export declare function lifecycleHelperUnit(options: Pick<UnitOptions, 'binPath' | 'lifecycleProfilePath' | 'lifecycleHelperSocketPath'>): string;
|
|
115
132
|
/**
|
|
116
133
|
* Exchange a tenant-issued one-time capability before the durable agent starts.
|
|
117
134
|
*
|
package/dist/provision.js
CHANGED
|
@@ -42,9 +42,104 @@ var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, s
|
|
|
42
42
|
var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
|
|
43
43
|
var DEPLOYMENT_GROUP = "forgezero-deploy";
|
|
44
44
|
var VAULT_GROUP = "forgezero-vault";
|
|
45
|
+
var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
45
46
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
46
47
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
47
48
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
49
|
+
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
50
|
+
var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
51
|
+
var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
|
|
52
|
+
var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
|
|
53
|
+
var systemdPath = (value, label) => {
|
|
54
|
+
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
55
|
+
throw new Error(`invalid ${label} path`);
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
function warpConfigUnit(options) {
|
|
59
|
+
if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
|
|
60
|
+
throw new Error("WARP organization is invalid");
|
|
61
|
+
}
|
|
62
|
+
if (!options.warpClientIdCredentialPath || !options.warpClientSecretCredentialPath) {
|
|
63
|
+
throw new Error("WARP service-token credential paths are required");
|
|
64
|
+
}
|
|
65
|
+
const clientIdPath = systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential");
|
|
66
|
+
const clientSecretPath = systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential");
|
|
67
|
+
const bin = options.binPath ?? "fz-agent";
|
|
68
|
+
return `[Unit]
|
|
69
|
+
Description=Materialize Cloudflare One enrollment in tmpfs
|
|
70
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
71
|
+
Before=warp-svc.service
|
|
72
|
+
|
|
73
|
+
[Service]
|
|
74
|
+
Type=oneshot
|
|
75
|
+
RemainAfterExit=yes
|
|
76
|
+
LoadCredentialEncrypted=warp-auth-client-id:${clientIdPath}
|
|
77
|
+
LoadCredentialEncrypted=warp-auth-client-secret:${clientSecretPath}
|
|
78
|
+
Environment=FZ_WARP_CLIENT_ID_CREDENTIAL=warp-auth-client-id
|
|
79
|
+
Environment=FZ_WARP_CLIENT_SECRET_CREDENTIAL=warp-auth-client-secret
|
|
80
|
+
ExecStart=${bin} warp-config --organization=${options.warpOrganization}
|
|
81
|
+
RuntimeDirectory=forgezero-warp
|
|
82
|
+
RuntimeDirectoryMode=0700
|
|
83
|
+
RuntimeDirectoryPreserve=yes
|
|
84
|
+
UMask=0077
|
|
85
|
+
LimitCORE=0
|
|
86
|
+
NoNewPrivileges=true
|
|
87
|
+
PrivateTmp=true
|
|
88
|
+
ProtectSystem=strict
|
|
89
|
+
ProtectHome=true
|
|
90
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
91
|
+
|
|
92
|
+
[Install]
|
|
93
|
+
WantedBy=multi-user.target
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function warpServiceDropIn() {
|
|
97
|
+
return `[Unit]
|
|
98
|
+
Requires=forgezero-warp-config.service
|
|
99
|
+
After=forgezero-warp-config.service
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
function lifecycleHelperUnit(options) {
|
|
103
|
+
if (!options.lifecycleProfilePath)
|
|
104
|
+
throw new Error("lifecycle helper needs a root-owned profile");
|
|
105
|
+
const bin = options.binPath ?? "fz-agent";
|
|
106
|
+
const profile = systemdPath(options.lifecycleProfilePath, "lifecycle profile");
|
|
107
|
+
const socket = systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket");
|
|
108
|
+
return `[Unit]
|
|
109
|
+
Description=ForgeZero fixed-operation compute lifecycle helper
|
|
110
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
111
|
+
After=network-online.target
|
|
112
|
+
Wants=network-online.target
|
|
113
|
+
|
|
114
|
+
[Service]
|
|
115
|
+
Type=simple
|
|
116
|
+
User=root
|
|
117
|
+
Group=${LIFECYCLE_GROUP}
|
|
118
|
+
Environment=FZ_LIFECYCLE_HELPER_SOCKET=${socket}
|
|
119
|
+
ExecStart=${bin} lifecycle-helper --profile=${profile}
|
|
120
|
+
Restart=always
|
|
121
|
+
RestartSec=2
|
|
122
|
+
RuntimeDirectory=forgezero-lifecycle
|
|
123
|
+
RuntimeDirectoryMode=0750
|
|
124
|
+
UMask=0007
|
|
125
|
+
LimitCORE=0
|
|
126
|
+
NoNewPrivileges=true
|
|
127
|
+
PrivateTmp=true
|
|
128
|
+
ProtectSystem=strict
|
|
129
|
+
ProtectHome=true
|
|
130
|
+
ProtectKernelTunables=true
|
|
131
|
+
ProtectKernelModules=true
|
|
132
|
+
ProtectControlGroups=true
|
|
133
|
+
RestrictSUIDSGID=true
|
|
134
|
+
RestrictRealtime=true
|
|
135
|
+
MemoryDenyWriteExecute=true
|
|
136
|
+
LockPersonality=true
|
|
137
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
138
|
+
|
|
139
|
+
[Install]
|
|
140
|
+
WantedBy=multi-user.target
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
48
143
|
function agentEnrolmentUnit(options) {
|
|
49
144
|
if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
|
|
50
145
|
throw new Error("direct enrolment needs API, credential and state paths");
|
|
@@ -139,6 +234,19 @@ function agentUnit(options) {
|
|
|
139
234
|
const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
|
|
140
235
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
141
236
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
237
|
+
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
238
|
+
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
239
|
+
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
240
|
+
}
|
|
241
|
+
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
242
|
+
const warpValues = [
|
|
243
|
+
options.warpOrganization,
|
|
244
|
+
options.warpClientIdCredentialPath,
|
|
245
|
+
options.warpClientSecretCredentialPath
|
|
246
|
+
];
|
|
247
|
+
const warpEnabled = warpValues.every(Boolean);
|
|
248
|
+
if (warpValues.some(Boolean) && !warpEnabled)
|
|
249
|
+
throw new Error("WARP configuration must be supplied together");
|
|
142
250
|
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
143
251
|
const deploymentEnvironment = options.deploymentEnvironment ?? {};
|
|
144
252
|
const deploymentCredentials = options.deploymentCredentials ?? {};
|
|
@@ -173,7 +281,9 @@ function agentUnit(options) {
|
|
|
173
281
|
Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
|
|
174
282
|
...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
|
|
175
283
|
options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
|
|
176
|
-
options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null
|
|
284
|
+
options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
|
|
285
|
+
options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
|
|
286
|
+
options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
|
|
177
287
|
].filter((line) => line !== null);
|
|
178
288
|
if (deploymentEnabled) {
|
|
179
289
|
environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
|
|
@@ -183,14 +293,22 @@ function agentUnit(options) {
|
|
|
183
293
|
const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
|
|
184
294
|
`);
|
|
185
295
|
const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
|
|
186
|
-
const
|
|
296
|
+
const supplementaryGroups = [
|
|
297
|
+
deploymentEnabled ? DEPLOYMENT_GROUP : null,
|
|
298
|
+
lifecycleEnabled ? LIFECYCLE_GROUP : null
|
|
299
|
+
].filter((value) => value !== null);
|
|
300
|
+
const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
|
|
187
301
|
const after = [
|
|
188
302
|
"network-online.target",
|
|
189
303
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
304
|
+
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
305
|
+
warpEnabled ? "warp-svc.service" : null,
|
|
190
306
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
191
307
|
].filter((value) => value !== null);
|
|
192
308
|
const requires = [
|
|
193
309
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
310
|
+
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
311
|
+
warpEnabled ? "warp-svc.service" : null,
|
|
194
312
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
195
313
|
].filter((value) => value !== null);
|
|
196
314
|
const deploymentDependency = [
|
|
@@ -262,37 +380,63 @@ function planProvision(options) {
|
|
|
262
380
|
const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
|
|
263
381
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
264
382
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
383
|
+
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
384
|
+
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
385
|
+
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
386
|
+
}
|
|
387
|
+
const warpValues = [
|
|
388
|
+
options.warpOrganization,
|
|
389
|
+
options.warpClientIdCredentialPath,
|
|
390
|
+
options.warpClientSecretCredentialPath
|
|
391
|
+
];
|
|
392
|
+
const warpEnabled = warpValues.every(Boolean);
|
|
393
|
+
if (warpValues.some(Boolean) && !warpEnabled)
|
|
394
|
+
throw new Error("WARP configuration must be supplied together");
|
|
265
395
|
const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
266
396
|
if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
|
|
267
397
|
throw new Error("direct enrolment paths must be supplied together");
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return value;
|
|
272
|
-
};
|
|
273
|
-
const enrolTokenSourcePath = enrolmentEnabled ? safePath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
274
|
-
const enrolTokenCredentialPath = enrolmentEnabled ? safePath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
275
|
-
const enrolStatePath = enrolmentEnabled ? safePath(options.enrolStatePath, "enrolment state") : undefined;
|
|
398
|
+
const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
399
|
+
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
400
|
+
const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
276
401
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
277
|
-
const sourceBinPath = options.sourceBinPath ?
|
|
278
|
-
const binPath = options.binPath ?
|
|
279
|
-
const gitCredentialPath = options.gitCredentialPath ?
|
|
280
|
-
const gitPublicKeyPath = options.gitPublicKeyPath ?
|
|
402
|
+
const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
|
|
403
|
+
const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
|
|
404
|
+
const gitCredentialPath = options.gitCredentialPath ? systemdPath(options.gitCredentialPath, "Git credential") : undefined;
|
|
405
|
+
const gitPublicKeyPath = options.gitPublicKeyPath ? systemdPath(options.gitPublicKeyPath, "Git public key") : undefined;
|
|
281
406
|
if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
|
|
282
407
|
throw new Error("generated Git identity needs credential and public-key paths");
|
|
283
408
|
}
|
|
284
409
|
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
410
|
+
const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
|
|
411
|
+
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
412
|
+
const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
|
|
413
|
+
const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
|
|
285
414
|
return {
|
|
286
415
|
mode,
|
|
287
416
|
reason: reasonFor(mode),
|
|
288
417
|
unitPath: UNIT_PATH,
|
|
289
|
-
unit: agentUnit({ ...options, mode }),
|
|
418
|
+
unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
|
|
290
419
|
auxiliaryUnits: [
|
|
291
420
|
...deploymentEnabled ? [
|
|
292
421
|
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
293
422
|
] : [],
|
|
294
423
|
...enrolmentEnabled ? [
|
|
295
424
|
{ path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
|
|
425
|
+
] : [],
|
|
426
|
+
...lifecycleEnabled ? [
|
|
427
|
+
{ path: LIFECYCLE_HELPER_UNIT_PATH, unit: lifecycleHelperUnit({
|
|
428
|
+
...options,
|
|
429
|
+
lifecycleProfilePath,
|
|
430
|
+
lifecycleHelperSocketPath
|
|
431
|
+
}) }
|
|
432
|
+
] : [],
|
|
433
|
+
...warpEnabled ? [
|
|
434
|
+
{ path: WARP_CONFIG_UNIT_PATH, unit: warpConfigUnit({
|
|
435
|
+
...options,
|
|
436
|
+
warpClientIdCredentialPath,
|
|
437
|
+
warpClientSecretCredentialPath
|
|
438
|
+
}) },
|
|
439
|
+
{ path: WARP_SERVICE_DROP_IN_PATH, unit: warpServiceDropIn() }
|
|
296
440
|
] : []
|
|
297
441
|
],
|
|
298
442
|
socketPath: options.socketPath,
|
|
@@ -306,10 +450,18 @@ function planProvision(options) {
|
|
|
306
450
|
label: "root-owned agent runtime",
|
|
307
451
|
command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
|
|
308
452
|
}] : [],
|
|
453
|
+
...warpEnabled ? [{
|
|
454
|
+
label: "Cloudflare One client for Ubuntu 26.04",
|
|
455
|
+
command: `. /etc/os-release; test "$ID" = ubuntu && test "$VERSION_ID" = 26.04; ` + `install -d -m 0755 /usr/share/keyrings /etc/apt/sources.list.d /etc/systemd/system/warp-svc.service.d; ` + `curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg -o /run/cloudflare-warp-key.gpg; ` + `gpg --batch --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg /run/cloudflare-warp-key.gpg; ` + `rm -f /run/cloudflare-warp-key.gpg; ` + `printf 'deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ %s main\\n' "$VERSION_CODENAME" > /etc/apt/sources.list.d/cloudflare-client.list; ` + `apt-get update -qq; DEBIAN_FRONTEND=noninteractive apt-get install -y cloudflare-warp`
|
|
456
|
+
}] : [],
|
|
309
457
|
...deploymentEnabled ? [{
|
|
310
458
|
label: "deployment isolation group",
|
|
311
459
|
command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
|
|
312
460
|
}] : [],
|
|
461
|
+
...lifecycleEnabled ? [{
|
|
462
|
+
label: "lifecycle helper access group",
|
|
463
|
+
command: `groupadd --system ${LIFECYCLE_GROUP} || true`
|
|
464
|
+
}] : [],
|
|
313
465
|
{
|
|
314
466
|
label: "service account",
|
|
315
467
|
command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
|
|
@@ -318,6 +470,10 @@ function planProvision(options) {
|
|
|
318
470
|
label: "bind service account to vault group",
|
|
319
471
|
command: `usermod -g ${VAULT_GROUP} ${user}`
|
|
320
472
|
},
|
|
473
|
+
...lifecycleEnabled ? [{
|
|
474
|
+
label: "grant lifecycle helper socket access",
|
|
475
|
+
command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
|
|
476
|
+
}] : [],
|
|
321
477
|
...deploymentEnabled ? [{
|
|
322
478
|
label: "credential-free deployment account",
|
|
323
479
|
command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
|
|
@@ -363,6 +519,8 @@ function planProvision(options) {
|
|
|
363
519
|
label: "enable and start",
|
|
364
520
|
command: `systemctl enable --now ${[
|
|
365
521
|
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
522
|
+
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
523
|
+
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
366
524
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
367
525
|
"forgezero-agent.service"
|
|
368
526
|
].join(" ")}`
|
|
@@ -377,6 +535,14 @@ function planProvision(options) {
|
|
|
377
535
|
label: "prove the deployment runner socket exists",
|
|
378
536
|
command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
|
|
379
537
|
}] : [],
|
|
538
|
+
...lifecycleEnabled ? [{
|
|
539
|
+
label: "prove the lifecycle helper socket exists",
|
|
540
|
+
command: `test -S ${lifecycleHelperSocketPath}`
|
|
541
|
+
}] : [],
|
|
542
|
+
...warpEnabled ? [{
|
|
543
|
+
label: "prove Cloudflare WARP is connected",
|
|
544
|
+
command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
|
|
545
|
+
}] : [],
|
|
380
546
|
...options.repository ? [{
|
|
381
547
|
label: "prove the deployment control socket exists",
|
|
382
548
|
command: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
|
|
@@ -385,15 +551,23 @@ function planProvision(options) {
|
|
|
385
551
|
};
|
|
386
552
|
}
|
|
387
553
|
export {
|
|
554
|
+
warpServiceDropIn,
|
|
555
|
+
warpConfigUnit,
|
|
388
556
|
reasonFor,
|
|
389
557
|
planProvision,
|
|
390
558
|
modeFor,
|
|
559
|
+
lifecycleHelperUnit,
|
|
391
560
|
deploymentRunnerUnit,
|
|
392
561
|
atLeast,
|
|
393
562
|
agentUnit,
|
|
394
563
|
agentEnrolmentUnit,
|
|
564
|
+
WARP_SERVICE_DROP_IN_PATH,
|
|
565
|
+
WARP_CONFIG_UNIT_PATH,
|
|
395
566
|
VAULT_GROUP,
|
|
396
567
|
UNIT_PATH,
|
|
568
|
+
LIFECYCLE_HELPER_UNIT_PATH,
|
|
569
|
+
LIFECYCLE_HELPER_SOCKET,
|
|
570
|
+
LIFECYCLE_GROUP,
|
|
397
571
|
ENROLMENT_UNIT_PATH,
|
|
398
572
|
DEPLOYMENT_RUNNER_USER,
|
|
399
573
|
DEPLOYMENT_RUNNER_UNIT_PATH,
|
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.
|
|
2
|
+
export declare const VERSION = "0.1.23";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Cloudflare One MDM document; callers keep both token values out of argv/env. */
|
|
2
|
+
export declare function renderWarpMdm(options: {
|
|
3
|
+
organization: string;
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
}): string;
|
|
7
|
+
/**
|
|
8
|
+
* Materialize enrollment only in /run. The durable /var/lib path is a symlink,
|
|
9
|
+
* so neither service-token half is left as plaintext on the filesystem.
|
|
10
|
+
*/
|
|
11
|
+
export declare function materializeWarpMdm(options: {
|
|
12
|
+
organization: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
clientSecret: string;
|
|
15
|
+
runtimePath?: string;
|
|
16
|
+
servicePath?: string;
|
|
17
|
+
}): {
|
|
18
|
+
runtimePath: string;
|
|
19
|
+
servicePath: string;
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
|
|
3
3
|
"name": "@forgezero/agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.23",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"check": "tsc --noEmit",
|
|
8
8
|
"prebuild": "rm -rf dist",
|
|
9
|
-
"build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm --packages external && bun build src/cli/index.ts --outfile dist/fz.js --target bun --format esm --packages external && bun build src/compute.ts src/provision.ts src/subscribe.ts src/pipeline.ts src/definition.ts src/ssh-server.ts src/ssh-listen.ts src/provisioning-pull.ts src/guest-enrolment.ts src/node-vault.ts src/metal-provision.ts src/metal-helper-socket.ts src/deployment-runner.ts src/ubuntu.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
|
|
9
|
+
"build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm --packages external && bun build src/cli/index.ts --outfile dist/fz.js --target bun --format esm --packages external && bun build src/compute.ts src/provision.ts src/subscribe.ts src/pipeline.ts src/definition.ts src/ssh-server.ts src/ssh-listen.ts src/provisioning-pull.ts src/migration-pull.ts src/guest-enrolment.ts src/node-vault.ts src/metal-provision.ts src/metal-helper-socket.ts src/lifecycle-helper.ts src/deployment-runner.ts src/ubuntu.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
|
|
10
10
|
"prepublishOnly": "bun run check && bun run build"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
@@ -88,6 +88,14 @@
|
|
|
88
88
|
"types": "./dist/provisioning-pull.d.ts",
|
|
89
89
|
"default": "./dist/provisioning-pull.js"
|
|
90
90
|
},
|
|
91
|
+
"./migration-pull": {
|
|
92
|
+
"types": "./dist/migration-pull.d.ts",
|
|
93
|
+
"default": "./dist/migration-pull.js"
|
|
94
|
+
},
|
|
95
|
+
"./lifecycle-helper": {
|
|
96
|
+
"types": "./dist/lifecycle-helper.d.ts",
|
|
97
|
+
"default": "./dist/lifecycle-helper.js"
|
|
98
|
+
},
|
|
91
99
|
"./metal-provision": {
|
|
92
100
|
"types": "./dist/metal-provision.d.ts",
|
|
93
101
|
"default": "./dist/metal-provision.js"
|