@forgezero/agent 0.1.124 → 0.1.128
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/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +3 -1
- package/dist/bootstrap.js +7 -4
- package/dist/community-rehearsal-host.d.ts +9 -0
- package/dist/community-rehearsal-host.js +120 -0
- package/dist/fz-agent.js +208 -30
- package/dist/fz.js +7554 -7326
- package/dist/host-maintenance.js +1 -1
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +32 -12
- package/dist/platform-fleet-verification.js +1 -1
- package/dist/provision.js +1 -1
- package/dist/recovery-host.d.ts +9 -0
- package/dist/recovery-host.js +27 -10
- package/dist/ssh-bootstrap.d.ts +8 -0
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
package/dist/agent-heartbeat.js
CHANGED
package/dist/bootstrap.d.ts
CHANGED
|
@@ -123,7 +123,9 @@ export interface EnrolledComputeBootstrapSecrets {
|
|
|
123
123
|
}
|
|
124
124
|
export type BootstrapSecrets = PlatformBootstrapSecrets | EnrolledComputeBootstrapSecrets;
|
|
125
125
|
export declare function validateEnrolledComputeBootstrapSecrets(config: EnrolledComputeActivationConfig, input: unknown): EnrolledComputeBootstrapSecrets;
|
|
126
|
-
export declare function validatePlatformBootstrapSecrets(config: PlatformBootstrapConfig, input: unknown
|
|
126
|
+
export declare function validatePlatformBootstrapSecrets(config: PlatformBootstrapConfig, input: unknown, options?: {
|
|
127
|
+
allowStoredCloudflareCredentials?: boolean;
|
|
128
|
+
}): PlatformBootstrapSecrets;
|
|
127
129
|
export interface BootstrapStep {
|
|
128
130
|
id: string;
|
|
129
131
|
label: string;
|
package/dist/bootstrap.js
CHANGED
|
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1460
1460
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1461
1461
|
|
|
1462
1462
|
// src/version.ts
|
|
1463
|
-
var VERSION = "0.1.
|
|
1463
|
+
var VERSION = "0.1.128";
|
|
1464
1464
|
|
|
1465
1465
|
// src/software.ts
|
|
1466
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3805,7 +3805,7 @@ function validateEnrolledComputeBootstrapSecrets(config, input) {
|
|
|
3805
3805
|
...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
|
|
3806
3806
|
};
|
|
3807
3807
|
}
|
|
3808
|
-
function validatePlatformBootstrapSecrets(config, input) {
|
|
3808
|
+
function validatePlatformBootstrapSecrets(config, input, options = {}) {
|
|
3809
3809
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
3810
3810
|
throw new Error("bootstrap credential input must be an object");
|
|
3811
3811
|
const source = input;
|
|
@@ -3833,7 +3833,8 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3833
3833
|
throw new Error("backup credential is malformed");
|
|
3834
3834
|
}
|
|
3835
3835
|
const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
|
|
3836
|
-
|
|
3836
|
+
const cloudflareCredentialsSupplied = Boolean(cloudflareTunnelToken || cloudflareApiToken);
|
|
3837
|
+
if (!cloudflareConfigured && cloudflareCredentialsSupplied || cloudflareConfigured && !cloudflareCredentialsSupplied && !options.allowStoredCloudflareCredentials || Boolean(cloudflareTunnelToken) !== Boolean(cloudflareApiToken)) {
|
|
3837
3838
|
throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
|
|
3838
3839
|
}
|
|
3839
3840
|
if (cloudflareTunnelToken)
|
|
@@ -4759,7 +4760,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4759
4760
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
4760
4761
|
if (!secrets)
|
|
4761
4762
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
4762
|
-
const checked4 = validatePlatformBootstrapSecrets(config, secrets
|
|
4763
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
4764
|
+
allowStoredCloudflareCredentials: Boolean(installed?.cloudflare && config.cloudflareHandoff && !cloudflare)
|
|
4765
|
+
});
|
|
4763
4766
|
return {
|
|
4764
4767
|
root: checked4.clusterBootstrapCode,
|
|
4765
4768
|
email: checked4.emailSecret,
|
|
@@ -9,6 +9,8 @@ export declare const COMMUNITY_REHEARSAL_COORDINATOR_PORT = 18529;
|
|
|
9
9
|
export declare const COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE = "18528:18539";
|
|
10
10
|
export declare const COMMUNITY_REHEARSAL_API_PORT = 18787;
|
|
11
11
|
export declare const COMMUNITY_REHEARSAL_FIREWALL_COMMENT = "fz-community-rehearsal";
|
|
12
|
+
export declare const COMMUNITY_REHEARSAL_EGRESS_TABLE = "forgezero_agent_egress";
|
|
13
|
+
export declare const COMMUNITY_REHEARSAL_EGRESS_CHAIN = "output";
|
|
12
14
|
export interface CommunityRehearsalNode {
|
|
13
15
|
name: string;
|
|
14
16
|
address: string;
|
|
@@ -48,6 +50,11 @@ export type CommunityHostOperation = {
|
|
|
48
50
|
path: typeof COMMUNITY_DATABASE_ROOT | typeof COMMUNITY_REHEARSAL_ROOT | typeof COMMUNITY_CREDENTIAL_PARENT;
|
|
49
51
|
} | {
|
|
50
52
|
kind: 'clear-firewall';
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'prepare-egress';
|
|
55
|
+
addresses: readonly string[];
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'clear-egress';
|
|
51
58
|
} | {
|
|
52
59
|
kind: 'write';
|
|
53
60
|
path: string;
|
|
@@ -75,5 +82,7 @@ export declare function planCommunityRehearsalPrepare(request: Extract<Community
|
|
|
75
82
|
/** Remove only the dedicated rehearsal units, credentials, release and data. */
|
|
76
83
|
export declare function planCommunityRehearsalCleanup(): CommunityHostOperation[];
|
|
77
84
|
export declare function communityRehearsalFirewallRuleNumbers(status: string): number[];
|
|
85
|
+
export declare function communityRehearsalEgressRuleHandles(ruleset: string): number[];
|
|
86
|
+
export declare function communityRehearsalEgressRuleArguments(uid: string, addresses: readonly string[]): readonly (readonly string[])[];
|
|
78
87
|
/** Execute only the dedicated four-node rehearsal operation selected by a typed request. */
|
|
79
88
|
export declare function runCommunityRehearsalHost(request: CommunityRehearsalHostRequest): Promise<Record<string, unknown>>;
|
|
@@ -577,6 +577,8 @@ var COMMUNITY_REHEARSAL_COORDINATOR_PORT = 18529;
|
|
|
577
577
|
var COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE = "18528:18539";
|
|
578
578
|
var COMMUNITY_REHEARSAL_API_PORT = 18787;
|
|
579
579
|
var COMMUNITY_REHEARSAL_FIREWALL_COMMENT = "fz-community-rehearsal";
|
|
580
|
+
var COMMUNITY_REHEARSAL_EGRESS_TABLE = "forgezero_agent_egress";
|
|
581
|
+
var COMMUNITY_REHEARSAL_EGRESS_CHAIN = "output";
|
|
580
582
|
var RELEASE = /^[a-f0-9]{64}$/;
|
|
581
583
|
var SAFE_NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
582
584
|
async function ensureCommunityRehearsalArango(execute = executeSoftwareOperation) {
|
|
@@ -740,6 +742,7 @@ function planCommunityRehearsalPrepare(request) {
|
|
|
740
742
|
{ kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-community-api.service"], accepted: [0, 5] },
|
|
741
743
|
{ kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-rehearsal-db.service"], accepted: [0, 5] },
|
|
742
744
|
{ kind: "clear-firewall" },
|
|
745
|
+
{ kind: "prepare-egress", addresses: topology.map(({ address }) => address) },
|
|
743
746
|
...topology.map(({ address }) => ({
|
|
744
747
|
kind: "exec",
|
|
745
748
|
argv: [
|
|
@@ -802,6 +805,7 @@ function planCommunityRehearsalCleanup() {
|
|
|
802
805
|
{ kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
|
|
803
806
|
{ kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
|
|
804
807
|
{ kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
|
|
808
|
+
{ kind: "clear-egress" },
|
|
805
809
|
{ kind: "clear-firewall" },
|
|
806
810
|
{ kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
|
|
807
811
|
{ kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
|
|
@@ -824,6 +828,10 @@ async function applyOperations(operations) {
|
|
|
824
828
|
rmSync2(operation.path, { recursive: true, force: true });
|
|
825
829
|
else if (operation.kind === "clear-firewall")
|
|
826
830
|
await clearCommunityRehearsalFirewall();
|
|
831
|
+
else if (operation.kind === "prepare-egress")
|
|
832
|
+
await prepareCommunityRehearsalEgress(operation.addresses);
|
|
833
|
+
else if (operation.kind === "clear-egress")
|
|
834
|
+
await clearCommunityRehearsalEgress();
|
|
827
835
|
else if (operation.kind === "write") {
|
|
828
836
|
mkdirSync2(dirname2(operation.path), { recursive: true });
|
|
829
837
|
writeFileSync2(operation.path, operation.content, { mode: operation.mode });
|
|
@@ -854,6 +862,49 @@ function communityRehearsalFirewallRuleNumbers(status) {
|
|
|
854
862
|
return match ? [Number(match[1])] : [];
|
|
855
863
|
}).filter((number) => Number.isSafeInteger(number) && number > 0).sort((left, right) => right - left);
|
|
856
864
|
}
|
|
865
|
+
function communityRehearsalEgressRuleHandles(ruleset) {
|
|
866
|
+
return ruleset.split(/\r?\n/).flatMap((line) => {
|
|
867
|
+
if (!line.includes(`comment "${COMMUNITY_REHEARSAL_FIREWALL_COMMENT}"`))
|
|
868
|
+
return [];
|
|
869
|
+
const match = line.match(/# handle (\d+)\s*$/);
|
|
870
|
+
return match ? [Number(match[1])] : [];
|
|
871
|
+
}).filter((number) => Number.isSafeInteger(number) && number > 0).sort((left, right) => right - left);
|
|
872
|
+
}
|
|
873
|
+
function communityRehearsalEgressRuleArguments(uid, addresses) {
|
|
874
|
+
if (!/^\d+$/.test(uid) || addresses.some((address) => isIP(address) !== 4) || new Set(addresses).size !== addresses.length) {
|
|
875
|
+
throw new Error("rehearsal egress arguments require one uid and unique IPv4 addresses");
|
|
876
|
+
}
|
|
877
|
+
return addresses.flatMap((address) => [
|
|
878
|
+
[
|
|
879
|
+
"meta",
|
|
880
|
+
"skuid",
|
|
881
|
+
uid,
|
|
882
|
+
"ct",
|
|
883
|
+
"state",
|
|
884
|
+
"established,related",
|
|
885
|
+
"ip",
|
|
886
|
+
"daddr",
|
|
887
|
+
address,
|
|
888
|
+
"accept",
|
|
889
|
+
"comment",
|
|
890
|
+
COMMUNITY_REHEARSAL_FIREWALL_COMMENT
|
|
891
|
+
],
|
|
892
|
+
[
|
|
893
|
+
"meta",
|
|
894
|
+
"skuid",
|
|
895
|
+
uid,
|
|
896
|
+
"ip",
|
|
897
|
+
"daddr",
|
|
898
|
+
address,
|
|
899
|
+
"tcp",
|
|
900
|
+
"dport",
|
|
901
|
+
String(COMMUNITY_REHEARSAL_COORDINATOR_PORT),
|
|
902
|
+
"accept",
|
|
903
|
+
"comment",
|
|
904
|
+
COMMUNITY_REHEARSAL_FIREWALL_COMMENT
|
|
905
|
+
]
|
|
906
|
+
]);
|
|
907
|
+
}
|
|
857
908
|
async function clearCommunityRehearsalFirewall() {
|
|
858
909
|
const status = await exec(["/usr/sbin/ufw", "status", "numbered"]);
|
|
859
910
|
if (status.exitCode !== 0)
|
|
@@ -865,6 +916,71 @@ async function clearCommunityRehearsalFirewall() {
|
|
|
865
916
|
}
|
|
866
917
|
}
|
|
867
918
|
}
|
|
919
|
+
var rehearsalEgressRules = async () => {
|
|
920
|
+
const result = await exec([
|
|
921
|
+
"/usr/sbin/nft",
|
|
922
|
+
"-a",
|
|
923
|
+
"list",
|
|
924
|
+
"chain",
|
|
925
|
+
"inet",
|
|
926
|
+
COMMUNITY_REHEARSAL_EGRESS_TABLE,
|
|
927
|
+
COMMUNITY_REHEARSAL_EGRESS_CHAIN
|
|
928
|
+
]);
|
|
929
|
+
if (result.exitCode === 0)
|
|
930
|
+
return { exists: true, output: result.output };
|
|
931
|
+
if (/No such file or directory|does not exist/i.test(result.output))
|
|
932
|
+
return { exists: false, output: result.output };
|
|
933
|
+
throw new Error(`unable to inspect rehearsal egress rules: ${result.output.trim()}`);
|
|
934
|
+
};
|
|
935
|
+
async function clearCommunityRehearsalEgress() {
|
|
936
|
+
const listed = await rehearsalEgressRules();
|
|
937
|
+
if (!listed.exists)
|
|
938
|
+
return;
|
|
939
|
+
for (const handle of communityRehearsalEgressRuleHandles(listed.output)) {
|
|
940
|
+
const removed = await exec([
|
|
941
|
+
"/usr/sbin/nft",
|
|
942
|
+
"delete",
|
|
943
|
+
"rule",
|
|
944
|
+
"inet",
|
|
945
|
+
COMMUNITY_REHEARSAL_EGRESS_TABLE,
|
|
946
|
+
COMMUNITY_REHEARSAL_EGRESS_CHAIN,
|
|
947
|
+
"handle",
|
|
948
|
+
String(handle)
|
|
949
|
+
]);
|
|
950
|
+
if (removed.exitCode !== 0) {
|
|
951
|
+
throw new Error(`unable to remove rehearsal egress rule ${handle}: ${removed.output.trim()}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function prepareCommunityRehearsalEgress(addresses) {
|
|
956
|
+
await clearCommunityRehearsalEgress();
|
|
957
|
+
if (!(await rehearsalEgressRules()).exists)
|
|
958
|
+
return;
|
|
959
|
+
const identity = await exec(["/usr/bin/id", "-u", "forgezero"]);
|
|
960
|
+
const uid = identity.output.trim();
|
|
961
|
+
if (identity.exitCode !== 0 || !/^\d+$/.test(uid))
|
|
962
|
+
throw new Error("unable to resolve the forgezero service uid");
|
|
963
|
+
for (const rule of communityRehearsalEgressRuleArguments(uid, addresses)) {
|
|
964
|
+
const inserted = await exec([
|
|
965
|
+
"/usr/sbin/nft",
|
|
966
|
+
"insert",
|
|
967
|
+
"rule",
|
|
968
|
+
"inet",
|
|
969
|
+
COMMUNITY_REHEARSAL_EGRESS_TABLE,
|
|
970
|
+
COMMUNITY_REHEARSAL_EGRESS_CHAIN,
|
|
971
|
+
...rule
|
|
972
|
+
]);
|
|
973
|
+
if (inserted.exitCode !== 0) {
|
|
974
|
+
await clearCommunityRehearsalEgress();
|
|
975
|
+
throw new Error(`unable to install a rehearsal egress rule: ${inserted.output.trim()}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const installed = communityRehearsalEgressRuleHandles((await rehearsalEgressRules()).output);
|
|
979
|
+
if (installed.length !== addresses.length * 2) {
|
|
980
|
+
await clearCommunityRehearsalEgress();
|
|
981
|
+
throw new Error("rehearsal egress rule verification failed");
|
|
982
|
+
}
|
|
983
|
+
}
|
|
868
984
|
var responseFor = async (request) => {
|
|
869
985
|
const node = request.topology.find((entry) => entry.name === request.node);
|
|
870
986
|
const base = `http://${node.address}:${COMMUNITY_REHEARSAL_API_PORT}`;
|
|
@@ -953,10 +1069,14 @@ export {
|
|
|
953
1069
|
parseCommunityRehearsalHostRequest,
|
|
954
1070
|
ensureCommunityRehearsalArango,
|
|
955
1071
|
communityRehearsalFirewallRuleNumbers,
|
|
1072
|
+
communityRehearsalEgressRuleHandles,
|
|
1073
|
+
communityRehearsalEgressRuleArguments,
|
|
956
1074
|
COMMUNITY_REHEARSAL_VERSION,
|
|
957
1075
|
COMMUNITY_REHEARSAL_STARTER_PORT,
|
|
958
1076
|
COMMUNITY_REHEARSAL_ROOT,
|
|
959
1077
|
COMMUNITY_REHEARSAL_FIREWALL_COMMENT,
|
|
1078
|
+
COMMUNITY_REHEARSAL_EGRESS_TABLE,
|
|
1079
|
+
COMMUNITY_REHEARSAL_EGRESS_CHAIN,
|
|
960
1080
|
COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE,
|
|
961
1081
|
COMMUNITY_REHEARSAL_COORDINATOR_PORT,
|
|
962
1082
|
COMMUNITY_REHEARSAL_API_PORT,
|