@serfab/cadre-host 1.12.0 → 1.14.0
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 +332 -423
- package/dist/bin/host.d.ts +6 -3
- package/dist/bin/host.js +301 -505
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/join-output.d.ts +56 -0
- package/dist/bin/join-output.js +92 -0
- package/dist/bin/join-output.js.map +1 -0
- package/dist/bin/nat-output.d.ts +49 -0
- package/dist/bin/nat-output.js +127 -0
- package/dist/bin/nat-output.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +44 -0
- package/dist/hosted/claim-details.js +66 -0
- package/dist/hosted/claim-details.js.map +1 -0
- package/dist/hosted/hosted-node-service.d.ts +318 -0
- package/dist/hosted/hosted-node-service.js +666 -0
- package/dist/hosted/hosted-node-service.js.map +1 -0
- package/dist/hosted/hosted-node-store.d.ts +40 -0
- package/dist/hosted/hosted-node-store.js +126 -0
- package/dist/hosted/hosted-node-store.js.map +1 -0
- package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
- package/dist/hosted/hosted-node-supervisor.js +396 -0
- package/dist/hosted/hosted-node-supervisor.js.map +1 -0
- package/dist/hosted/hosted-node-watcher.d.ts +83 -0
- package/dist/hosted/hosted-node-watcher.js +224 -0
- package/dist/hosted/hosted-node-watcher.js.map +1 -0
- package/dist/hosted/index.d.ts +16 -0
- package/dist/hosted/index.js +13 -0
- package/dist/hosted/index.js.map +1 -0
- package/dist/hosted/node-status.d.ts +35 -0
- package/dist/hosted/node-status.js +61 -0
- package/dist/hosted/node-status.js.map +1 -0
- package/dist/hosted/types.d.ts +134 -0
- package/dist/hosted/types.js +22 -0
- package/dist/hosted/types.js.map +1 -0
- package/dist/index.d.ts +12 -24
- package/dist/index.js +9 -19
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +4 -34
- package/dist/installer/config.js +3 -18
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +7 -19
- package/dist/installer/index.js +14 -69
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/paths.d.ts +0 -2
- package/dist/installer/paths.js +0 -4
- package/dist/installer/paths.js.map +1 -1
- package/dist/installer/wizard.d.ts +0 -7
- package/dist/installer/wizard.js +1 -8
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/address-resolver.d.ts +18 -17
- package/dist/nat/address-resolver.js +79 -25
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/nat/address-watch.d.ts +37 -0
- package/dist/nat/address-watch.js +84 -0
- package/dist/nat/address-watch.js.map +1 -0
- package/dist/nat/ddns/duckdns.d.ts +2 -1
- package/dist/nat/ddns/duckdns.js +5 -1
- package/dist/nat/ddns/duckdns.js.map +1 -1
- package/dist/nat/ddns/index.d.ts +6 -0
- package/dist/nat/ddns/index.js.map +1 -1
- package/dist/nat/index.d.ts +12 -9
- package/dist/nat/index.js +6 -5
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +203 -77
- package/dist/nat/nat-service.js +602 -237
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.d.ts +12 -5
- package/dist/nat/nat-store.js +114 -30
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/port-mapper.d.ts +83 -79
- package/dist/nat/port-mapper.js +148 -189
- package/dist/nat/port-mapper.js.map +1 -1
- package/dist/nat/reachability.d.ts +31 -15
- package/dist/nat/reachability.js +97 -22
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +78 -27
- package/dist/nat/types.js +8 -5
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +29 -71
- package/dist/orchestrator/host-process-orchestrator.js +78 -218
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
- package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
- package/dist/orchestrator/identity-file.js.map +1 -0
- package/dist/orchestrator/index.d.ts +2 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +6 -6
- package/dist/orchestrator/node-identity.js +7 -7
- package/dist/orchestrator/node-identity.js.map +1 -1
- package/dist/orchestrator/port-allocator.d.ts +5 -6
- package/dist/orchestrator/port-allocator.js +9 -11
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +3 -5
- package/dist/orchestrator/state-store.js +2 -10
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +33 -22
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/server/error-handler.d.ts +5 -25
- package/dist/server/error-handler.js +11 -74
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +7 -11
- package/dist/server/events/types.js +2 -1
- package/dist/server/events/types.js.map +1 -1
- package/dist/server/index.d.ts +11 -37
- package/dist/server/index.js +30 -48
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/hosted-nodes.d.ts +28 -0
- package/dist/server/routes/hosted-nodes.js +68 -0
- package/dist/server/routes/hosted-nodes.js.map +1 -0
- package/dist/server/routes/nat.d.ts +4 -1
- package/dist/server/routes/nat.js +13 -2
- package/dist/server/routes/nat.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +6 -20
- package/dist/server/routes/nodes.js +7 -86
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +2 -7
- package/dist/server/routes/settings.js +3 -7
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +2 -25
- package/dist/server/routes/status.js +1 -11
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/server.js +8 -8
- package/dist/server/settings-store.d.ts +2 -2
- package/dist/server/settings-store.js +2 -2
- package/dist/server/static.js +1 -2
- package/dist/server/static.js.map +1 -1
- package/dist/ui/assets/index-4FWuU8ye.js +19 -0
- package/dist/ui/assets/index-4FWuU8ye.js.map +1 -0
- package/dist/ui/assets/index-Di9Q4d01.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/store.d.ts +1 -1
- package/dist/update/store.js +1 -1
- package/dist/update/types.d.ts +1 -1
- package/dist/update/types.js +1 -1
- package/package.json +5 -6
- package/src/__tests__/cli-nat.smoke.test.ts +219 -212
- package/src/__tests__/cli.smoke.test.ts +1 -1
- package/src/__tests__/global-setup.ts +2 -3
- package/src/__tests__/orchestrator-claim-secret.test.ts +112 -0
- package/src/__tests__/orchestrator-handle-contract.ts +134 -134
- package/src/__tests__/orchestrator-node-identity.test.ts +182 -203
- package/src/__tests__/orchestrator-ports.test.ts +124 -143
- package/src/__tests__/orchestrator-push.test.ts +138 -146
- package/src/__tests__/orchestrator.test.ts +1019 -1063
- package/src/bin/host.ts +368 -553
- package/src/bin/join-output.ts +120 -0
- package/src/bin/nat-output.ts +177 -0
- package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +173 -173
- package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +265 -243
- package/src/hosted/__tests__/hosted-node-service.test.ts +609 -0
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +474 -0
- package/src/hosted/__tests__/test-invitation.ts +33 -0
- package/src/hosted/claim-details.ts +96 -0
- package/src/hosted/hosted-node-service.ts +795 -0
- package/src/hosted/hosted-node-store.ts +144 -0
- package/src/hosted/hosted-node-supervisor.ts +439 -0
- package/src/hosted/hosted-node-watcher.ts +250 -0
- package/src/hosted/index.ts +53 -0
- package/src/hosted/node-status.ts +71 -0
- package/src/hosted/types.ts +153 -0
- package/src/index.ts +72 -91
- package/src/installer/__tests__/config.test.ts +15 -40
- package/src/installer/__tests__/installer.smoke.test.ts +5 -86
- package/src/installer/__tests__/wizard.test.ts +56 -64
- package/src/installer/config.ts +135 -175
- package/src/installer/index.ts +15 -83
- package/src/installer/paths.ts +104 -109
- package/src/installer/wizard.ts +87 -103
- package/src/nat/__tests__/address-resolver.test.ts +107 -82
- package/src/nat/__tests__/nat-service.test.ts +433 -408
- package/src/nat/__tests__/nat-store.test.ts +127 -93
- package/src/nat/__tests__/port-mapper.test.ts +30 -191
- package/src/nat/__tests__/reachability.test.ts +96 -13
- package/src/nat/address-resolver.ts +94 -52
- package/src/nat/address-watch.ts +92 -0
- package/src/nat/ddns/duckdns.ts +5 -1
- package/src/nat/ddns/index.ts +6 -0
- package/src/nat/index.ts +87 -71
- package/src/nat/nat-service.ts +889 -501
- package/src/nat/nat-store.ts +237 -154
- package/src/nat/port-mapper.ts +241 -329
- package/src/nat/reachability.ts +134 -25
- package/src/nat/types.ts +209 -159
- package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +81 -243
- package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
- package/src/orchestrator/index.ts +9 -10
- package/src/orchestrator/node-identity.ts +41 -41
- package/src/orchestrator/port-allocator.ts +71 -73
- package/src/orchestrator/state-store.ts +76 -90
- package/src/orchestrator/types.ts +34 -23
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +81 -118
- package/src/server/__tests__/fakes.ts +40 -44
- package/src/server/__tests__/hosted-nodes-route.test.ts +160 -0
- package/src/server/__tests__/nodes-route.test.ts +114 -283
- package/src/server/__tests__/publishers.test.ts +193 -180
- package/src/server/__tests__/server.smoke.test.ts +134 -223
- package/src/server/__tests__/settings-route.test.ts +179 -195
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/status-route.test.ts +82 -121
- package/src/server/error-handler.ts +112 -178
- package/src/server/events/types.ts +20 -24
- package/src/server/index.ts +223 -267
- package/src/server/routes/hosted-nodes.ts +83 -0
- package/src/server/routes/nat.ts +75 -63
- package/src/server/routes/nodes.ts +96 -188
- package/src/server/routes/settings.ts +115 -123
- package/src/server/routes/status.ts +102 -134
- package/src/server/server.ts +8 -8
- package/src/server/settings-store.ts +2 -2
- package/src/server/static.ts +1 -2
- package/src/update/index.ts +2 -2
- package/src/update/store.ts +1 -1
- package/src/update/types.ts +1 -1
- package/dist/auth/duration.d.ts +0 -1
- package/dist/auth/duration.js +0 -32
- package/dist/auth/duration.js.map +0 -1
- package/dist/auth/index.d.ts +0 -11
- package/dist/auth/index.js +0 -9
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/self-label.d.ts +0 -36
- package/dist/auth/self-label.js +0 -57
- package/dist/auth/self-label.js.map +0 -1
- package/dist/auth/trust-circle-store.d.ts +0 -26
- package/dist/auth/trust-circle-store.js +0 -129
- package/dist/auth/trust-circle-store.js.map +0 -1
- package/dist/auth/trust-circle.d.ts +0 -156
- package/dist/auth/trust-circle.js +0 -307
- package/dist/auth/trust-circle.js.map +0 -1
- package/dist/auth/types.d.ts +0 -84
- package/dist/auth/types.js +0 -24
- package/dist/auth/types.js.map +0 -1
- package/dist/donation/donation-service.d.ts +0 -389
- package/dist/donation/donation-service.js +0 -828
- package/dist/donation/donation-service.js.map +0 -1
- package/dist/donation/donation-store.d.ts +0 -51
- package/dist/donation/donation-store.js +0 -152
- package/dist/donation/donation-store.js.map +0 -1
- package/dist/donation/donation-supervisor.js +0 -364
- package/dist/donation/donation-supervisor.js.map +0 -1
- package/dist/donation/grant-service.d.ts +0 -80
- package/dist/donation/grant-service.js +0 -210
- package/dist/donation/grant-service.js.map +0 -1
- package/dist/donation/grant-store.d.ts +0 -38
- package/dist/donation/grant-store.js +0 -135
- package/dist/donation/grant-store.js.map +0 -1
- package/dist/donation/index.d.ts +0 -20
- package/dist/donation/index.js +0 -17
- package/dist/donation/index.js.map +0 -1
- package/dist/donation/types.d.ts +0 -224
- package/dist/donation/types.js +0 -40
- package/dist/donation/types.js.map +0 -1
- package/dist/installer/identity.js.map +0 -1
- package/dist/owner/index.d.ts +0 -11
- package/dist/owner/index.js +0 -11
- package/dist/owner/index.js.map +0 -1
- package/dist/owner/owner-node-client.d.ts +0 -110
- package/dist/owner/owner-node-client.js +0 -184
- package/dist/owner/owner-node-client.js.map +0 -1
- package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
- package/dist/server/routes/bootstrap-node-validation.js +0 -197
- package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
- package/dist/server/routes/grants-admin.d.ts +0 -29
- package/dist/server/routes/grants-admin.js +0 -58
- package/dist/server/routes/grants-admin.js.map +0 -1
- package/dist/server/routes/grants.d.ts +0 -28
- package/dist/server/routes/grants.js +0 -165
- package/dist/server/routes/grants.js.map +0 -1
- package/dist/server/routes/provision-request-validation.d.ts +0 -45
- package/dist/server/routes/provision-request-validation.js +0 -97
- package/dist/server/routes/provision-request-validation.js.map +0 -1
- package/dist/server/routes/strands.d.ts +0 -20
- package/dist/server/routes/strands.js +0 -38
- package/dist/server/routes/strands.js.map +0 -1
- package/dist/server/routes/trust-circle.d.ts +0 -17
- package/dist/server/routes/trust-circle.js +0 -35
- package/dist/server/routes/trust-circle.js.map +0 -1
- package/dist/strands/index.d.ts +0 -7
- package/dist/strands/index.js +0 -6
- package/dist/strands/index.js.map +0 -1
- package/dist/strands/strand-service.d.ts +0 -50
- package/dist/strands/strand-service.js +0 -103
- package/dist/strands/strand-service.js.map +0 -1
- package/dist/strands/types.d.ts +0 -93
- package/dist/strands/types.js +0 -23
- package/dist/strands/types.js.map +0 -1
- package/dist/ui/assets/index-C6nELNfC.js +0 -23
- package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
- package/dist/ui/assets/index-TtIYSeZO.css +0 -1
- package/src/__tests__/cli-invite.smoke.test.ts +0 -192
- package/src/__tests__/host-owner.smoke.test.ts +0 -123
- package/src/__tests__/orchestrator-owner.test.ts +0 -234
- package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
- package/src/auth/__tests__/duration.test.ts +0 -28
- package/src/auth/__tests__/self-label.test.ts +0 -85
- package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
- package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
- package/src/auth/__tests__/trust-circle.test.ts +0 -445
- package/src/auth/duration.ts +0 -33
- package/src/auth/index.ts +0 -21
- package/src/auth/self-label.ts +0 -74
- package/src/auth/trust-circle-store.ts +0 -155
- package/src/auth/trust-circle.ts +0 -395
- package/src/auth/types.ts +0 -99
- package/src/donation/__tests__/donation-service.test.ts +0 -1193
- package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
- package/src/donation/__tests__/grant-service.test.ts +0 -213
- package/src/donation/__tests__/grant-store.test.ts +0 -113
- package/src/donation/donation-service.ts +0 -1033
- package/src/donation/donation-store.ts +0 -176
- package/src/donation/donation-supervisor.ts +0 -411
- package/src/donation/grant-service.ts +0 -252
- package/src/donation/grant-store.ts +0 -152
- package/src/donation/index.ts +0 -65
- package/src/donation/types.ts +0 -260
- package/src/owner/__tests__/owner-node-client.test.ts +0 -194
- package/src/owner/index.ts +0 -12
- package/src/owner/owner-node-client.ts +0 -259
- package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
- package/src/server/__tests__/grants-admin-route.test.ts +0 -171
- package/src/server/__tests__/grants-route.test.ts +0 -394
- package/src/server/__tests__/strands-route.test.ts +0 -234
- package/src/server/routes/bootstrap-node-validation.ts +0 -206
- package/src/server/routes/grants-admin.ts +0 -74
- package/src/server/routes/grants.ts +0 -193
- package/src/server/routes/provision-request-validation.ts +0 -105
- package/src/server/routes/strands.ts +0 -54
- package/src/server/routes/trust-circle.ts +0 -49
- package/src/strands/__tests__/strand-service.test.ts +0 -165
- package/src/strands/index.ts +0 -14
- package/src/strands/strand-service.ts +0 -135
- package/src/strands/types.ts +0 -102
|
@@ -40,9 +40,9 @@ import type { PushCredentials } from '@serfab/cadre-core';
|
|
|
40
40
|
import { strandFilterConfigFromText, type CliConfig } from '@serfab/cadre-cli';
|
|
41
41
|
import {
|
|
42
42
|
encodeDockerId,
|
|
43
|
-
type OwnerSpawnConfig,
|
|
44
43
|
type Handle,
|
|
45
44
|
type HostProcessConfig,
|
|
45
|
+
type HostedSpawnRequest,
|
|
46
46
|
type ManagedNodeInfo,
|
|
47
47
|
type NodePorts,
|
|
48
48
|
type NodeStateListener,
|
|
@@ -84,6 +84,17 @@ function scrubbedParentEnv(): NodeJS.ProcessEnv {
|
|
|
84
84
|
return env;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The env carrying how the child gets into its cadre, and nowhere else (see
|
|
89
|
+
* `HostedSpawnRequest`): `CADRE_CLAIM_SECRET` makes `cadre-cli start` wait to be
|
|
90
|
+
* claimed, `CADRE_INVITATION` makes it redeem the invitation once it is up.
|
|
91
|
+
*/
|
|
92
|
+
function joinEnv(request: HostedSpawnRequest): Record<string, string> | undefined {
|
|
93
|
+
if (request.claimSecret) return { CADRE_CLAIM_SECRET: request.claimSecret };
|
|
94
|
+
if (request.invitation) return { CADRE_INVITATION: request.invitation };
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
/**
|
|
88
99
|
* The libp2p listen addresses a managed child binds — its `CADRE_LISTEN_ADDRS`, one
|
|
89
100
|
* entry per element: TCP on the `p2p` port and WebSocket on the `ws` port, both on
|
|
@@ -103,30 +114,17 @@ export function childListenAddrs(ports: Pick<NodePorts, 'p2p' | 'ws'>): string[]
|
|
|
103
114
|
/**
|
|
104
115
|
* Every TCP port a managed child binds, on the address it binds it to: health and
|
|
105
116
|
* metrics (`cadre-cli`'s `HealthServer`) and the libp2p TCP and WebSocket listeners
|
|
106
|
-
* ({@link childListenAddrs}) on every interface
|
|
107
|
-
*
|
|
108
|
-
* set lacks (a handle persisted by an older build) is skipped.
|
|
117
|
+
* ({@link childListenAddrs}), all on every interface. A key the set lacks (a handle
|
|
118
|
+
* persisted by an older build) is skipped.
|
|
109
119
|
*/
|
|
110
|
-
function childPortBindings(ports: Partial<NodePorts
|
|
120
|
+
function childPortBindings(ports: Partial<NodePorts>): PortBinding[] {
|
|
111
121
|
const bindings: PortBinding[] = [];
|
|
112
122
|
for (const port of [ports.health, ports.metrics, ports.p2p, ports.ws]) {
|
|
113
123
|
if (port !== undefined) bindings.push({ port, host: '0.0.0.0' });
|
|
114
124
|
}
|
|
115
|
-
if (owner && ports.admin !== undefined) bindings.push({ port: ports.admin, host: '127.0.0.1' });
|
|
116
125
|
return bindings;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
/** Fixed friendly id for the admin's owner node. */
|
|
120
|
-
export const OWNER_CONTAINER_ID = 'owner';
|
|
121
|
-
|
|
122
|
-
/** Endpoint + bearer for the owner node's loopback admin channel (6.6). */
|
|
123
|
-
export interface OwnerAdminEndpoint {
|
|
124
|
-
/** e.g. `http://127.0.0.1:<adminPort>`. */
|
|
125
|
-
baseUrl: string;
|
|
126
|
-
/** The child's `CADRE_STARTUP_TOKEN`, used as the admin bearer secret. */
|
|
127
|
-
token: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
128
|
/**
|
|
131
129
|
* Runs cadre nodes as plain Node child processes on the host.
|
|
132
130
|
*
|
|
@@ -169,10 +167,10 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
169
167
|
private readonly cliEntrypoint: string;
|
|
170
168
|
/** Resolves push credentials fresh on every spawn (see HostProcessConfig). */
|
|
171
169
|
private readonly pushResolver?: PushCredentialsResolver;
|
|
170
|
+
/** The public addresses a node announces, asked at every spawn (see HostProcessConfig). */
|
|
171
|
+
private readonly announceAddrs?: HostProcessConfig['announceAddrs'];
|
|
172
172
|
/** State-change listeners — invoked when a handle's alive state changes. */
|
|
173
173
|
private readonly stateListeners = new Set<NodeStateListener>();
|
|
174
|
-
/** Persisted spawn config for the owner node (re-spawn on demand). */
|
|
175
|
-
private ownerConfig?: OwnerSpawnConfig;
|
|
176
174
|
|
|
177
175
|
constructor(private readonly cfg: HostProcessConfig) {
|
|
178
176
|
this.rootDir = resolvePath(cfg.rootDir);
|
|
@@ -188,6 +186,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
188
186
|
this.defaultMemoryLimit = cfg.defaultResources?.memoryLimit;
|
|
189
187
|
this.cliEntrypoint = cfg.spawn?.entrypoint ?? resolveCadreCliBin();
|
|
190
188
|
this.pushResolver = cfg.pushResolver;
|
|
189
|
+
this.announceAddrs = cfg.announceAddrs;
|
|
191
190
|
log('HostProcessOrchestrator rootDir=%s cli=%s', this.rootDir, this.cliEntrypoint);
|
|
192
191
|
}
|
|
193
192
|
|
|
@@ -198,7 +197,6 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
198
197
|
*/
|
|
199
198
|
async init(): Promise<void> {
|
|
200
199
|
const state = this.stateStore.load();
|
|
201
|
-
this.ownerConfig = state.ownerConfig;
|
|
202
200
|
for (const persisted of state.handles) {
|
|
203
201
|
const handle: Handle = {
|
|
204
202
|
containerId: persisted.containerId,
|
|
@@ -210,7 +208,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
210
208
|
spawnedAt: persisted.spawnedAt,
|
|
211
209
|
partyId: persisted.partyId,
|
|
212
210
|
profile: persisted.profile,
|
|
213
|
-
|
|
211
|
+
announcedAddrs: persisted.announcedAddrs ?? [],
|
|
214
212
|
alive: false,
|
|
215
213
|
};
|
|
216
214
|
handle.alive = isHandleLive(handle);
|
|
@@ -256,7 +254,8 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
256
254
|
|
|
257
255
|
/**
|
|
258
256
|
* Register a state-change listener. Invoked when a handle's `alive`
|
|
259
|
-
* transitions (child exit, manual stop)
|
|
257
|
+
* transitions (create, child exit, manual stop) and once more, as `stopped`,
|
|
258
|
+
* when a handle is removed. Returns an unsubscribe fn.
|
|
260
259
|
*/
|
|
261
260
|
onStateChange(listener: NodeStateListener): () => void {
|
|
262
261
|
this.stateListeners.add(listener);
|
|
@@ -264,18 +263,24 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
264
263
|
}
|
|
265
264
|
|
|
266
265
|
/**
|
|
267
|
-
* Spawn a
|
|
268
|
-
*
|
|
269
|
-
*
|
|
266
|
+
* Spawn a managed node — a hosted node for some cadre, started waiting to be
|
|
267
|
+
* claimed when the request carries a `claimSecret`, or redeeming the cadre
|
|
268
|
+
* invitation it carries as `invitation`. The node is given its own
|
|
269
|
+
* protobuf identity key inside its workdir (`ensureNodeIdentity`, reused across
|
|
270
270
|
* re-spawns of the same containerId), which is what makes its peer id stable
|
|
271
271
|
* across restarts AND what makes its node-local stores durable: `cadre-cli
|
|
272
272
|
* start` opens the file-backed bootstrap-peer, trusted-owner and strand
|
|
273
273
|
* network-state stores in the node's state directory, which defaults to the
|
|
274
274
|
* directory holding the `cadre.json` written here — the same workdir.
|
|
275
|
-
*
|
|
275
|
+
* Removing the node (`removeContainer`) deletes the workdir, so the key and
|
|
276
276
|
* every store go with it.
|
|
277
277
|
*/
|
|
278
|
-
async createContainer(request:
|
|
278
|
+
async createContainer(request: HostedSpawnRequest): Promise<OrchestratorCreateResult> {
|
|
279
|
+
// Before anything is allocated: `cadre-cli start` refuses the pair anyway, and a child
|
|
280
|
+
// that exits at once would only surface as a crash loop.
|
|
281
|
+
if (request.claimSecret && request.invitation) {
|
|
282
|
+
throw new Error(`container ${request.containerId}: a spawn carries a claim secret or an invitation, not both`);
|
|
283
|
+
}
|
|
279
284
|
const workdir = this.workdirFor(request.containerId);
|
|
280
285
|
// Sampled BEFORE `ensureNodeIdentity`, which brings the directory into
|
|
281
286
|
// existence as a side effect of writing the key. Only a spawn that found
|
|
@@ -296,26 +301,15 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
296
301
|
// node's worth of ports from a bounded range on every failed provision attempt.
|
|
297
302
|
const identity = await ensureNodeIdentity(workdir);
|
|
298
303
|
log('container %s identity peerId=%s', request.containerId, identity.peerId);
|
|
299
|
-
// A node started with pinned owner keys belongs to a FOREIGN cadre (the
|
|
300
|
-
// node-donation flow: it trusts the requester's owner key, not the host's).
|
|
301
|
-
// The host's FCM/APNs credentials are minted for the host owner's own app
|
|
302
|
-
// and are meaningless to a foreign cadre — so donated nodes get NO push
|
|
303
|
-
// block. (Per-grantee push creds would be a future ticket; none in v1.)
|
|
304
|
-
const foreignParty = (request.pinnedOwnerKeys?.length ?? 0) > 0;
|
|
305
304
|
// Only storage-profile nodes participate in strands and thus fan out push
|
|
306
305
|
// wakes — a transaction-only node need not carry credentials. Resolved
|
|
307
306
|
// BEFORE the drop below, which keeps the drop → launch window synchronous
|
|
308
307
|
// (see restoreDroppedHandles).
|
|
309
|
-
const push = request.profile === 'storage'
|
|
310
|
-
|
|
311
|
-
// `cadre-cli start` unions it into its cold-start pinnedKeyTrustPolicy so the
|
|
312
|
-
// node will accept the foreign-authority-signed seed presented via POST /seed.
|
|
313
|
-
const extraEnv = request.pinnedOwnerKeys?.length
|
|
314
|
-
? { CADRE_OWNER_KEYS: request.pinnedOwnerKeys.join(',') }
|
|
315
|
-
: undefined;
|
|
308
|
+
const push = request.profile === 'storage' ? await this.resolvePush() : undefined;
|
|
309
|
+
const extraEnv = joinEnv(request);
|
|
316
310
|
|
|
317
311
|
// The last `await` on this path: checked as close to the drop as it can be.
|
|
318
|
-
await this.refuseRespawnOverLiveChild(request.containerId
|
|
312
|
+
await this.refuseRespawnOverLiveChild(request.containerId);
|
|
319
313
|
|
|
320
314
|
// Nothing from here to the launch is `await`ed — which is what keeps
|
|
321
315
|
// `restoreDroppedHandles`'s documented precondition true even though the
|
|
@@ -331,7 +325,6 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
331
325
|
partyId: request.partyId,
|
|
332
326
|
profile: request.profile,
|
|
333
327
|
ports,
|
|
334
|
-
owner: false,
|
|
335
328
|
buildConfig: () => this.buildChildConfig(request, workdir, push),
|
|
336
329
|
extraArgs: ['--identity-file', identity.path],
|
|
337
330
|
...(extraEnv ? { extraEnv } : {}),
|
|
@@ -357,7 +350,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
357
350
|
|
|
358
351
|
/**
|
|
359
352
|
* Remove the working directory a spawn created for `containerId` when no
|
|
360
|
-
* handle owns it — the cleanup of last resort for a
|
|
353
|
+
* handle owns it — the cleanup of last resort for a hosted-node record that
|
|
361
354
|
* never got a `dockerId` (the host died between `ensureNodeIdentity` and the
|
|
362
355
|
* handle write, so `createContainer`'s own unwind never ran). Returns whether
|
|
363
356
|
* anything was removed.
|
|
@@ -366,15 +359,11 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
366
359
|
* to await.
|
|
367
360
|
*/
|
|
368
361
|
reclaimWorkdir(containerId: string): boolean {
|
|
369
|
-
// The admin's own node is never reclaimed by this route: its workdir is a
|
|
370
|
-
// single fixed path reused by every restart and it holds the host's own
|
|
371
|
-
// control-DB storage.
|
|
372
|
-
if (containerId === OWNER_CONTAINER_ID) return false;
|
|
373
362
|
// A live handle owns that directory, and `removeContainer` — which stops
|
|
374
363
|
// the child first — is the only thing allowed to delete it.
|
|
375
364
|
if (this.resolveDockerId(containerId)) return false;
|
|
376
365
|
const workdir = this.workdirFor(containerId);
|
|
377
|
-
// Defence, not a live case:
|
|
366
|
+
// Defence, not a live case: hosted-node ids are `hn_<base64url>` and so can
|
|
378
367
|
// hold neither a path separator nor a dot. A caller that ever passes
|
|
379
368
|
// something else must not be able to walk out of `rootDir`.
|
|
380
369
|
if (!workdir.startsWith(this.rootDir + sep)) {
|
|
@@ -436,123 +425,26 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
436
425
|
}
|
|
437
426
|
|
|
438
427
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* orchestrator restart) can re-spawn it without re-supplying them.
|
|
444
|
-
*
|
|
445
|
-
* The owner node is spawned as `cadre-cli start --owner
|
|
446
|
-
* --admin-port <p> --identity-file <identityPath>` so it carries the
|
|
447
|
-
* host's libp2p identity, founds/joins the control network, and exposes the
|
|
448
|
-
* loopback admin channel the manager delegates to.
|
|
428
|
+
* The public addresses the node about to spawn announces. A hook that throws costs
|
|
429
|
+
* the node its public addresses, never its start: it still binds and is reachable
|
|
430
|
+
* on its LAN addresses, and `NatService` restarts it once the addresses it should
|
|
431
|
+
* announce differ from these.
|
|
449
432
|
*/
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (!config) {
|
|
453
|
-
throw new Error('ensureOwnerNode: no owner config supplied and none persisted');
|
|
454
|
-
}
|
|
455
|
-
this.ownerConfig = config;
|
|
456
|
-
|
|
457
|
-
const existing = this.findOwnerHandle();
|
|
458
|
-
if (existing && isHandleLive(existing)) {
|
|
459
|
-
this.persist();
|
|
460
|
-
return toNodeInfo(existing);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const profile = config.profile ?? 'storage';
|
|
464
|
-
// Re-resolve push credentials from the secret store on every (re-)spawn so a
|
|
465
|
-
// restart picks up rotated keys and nothing raw is replayed from state.json.
|
|
466
|
-
// Resolved BEFORE the drop below so the drop → launch window stays
|
|
467
|
-
// synchronous (see restoreDroppedHandles).
|
|
468
|
-
const push = await this.resolvePush();
|
|
469
|
-
// The last `await` before the drop — same guard as in createContainer.
|
|
470
|
-
await this.refuseRespawnOverLiveChild(OWNER_CONTAINER_ID, { p2p: config.libp2pPort }, true);
|
|
471
|
-
|
|
472
|
-
// NOTE: the short-circuit above matches via `findOwnerHandle` (owner flag OR
|
|
473
|
-
// containerId), this drop only by containerId. Equivalent today — every
|
|
474
|
-
// owner handle is launched with OWNER_CONTAINER_ID. If an owner spawn ever
|
|
475
|
-
// takes a different containerId, the two would disagree and the handle the
|
|
476
|
-
// short-circuit found would survive here, leaking it and its ports.
|
|
477
|
-
const dropped = this.dropStaleHandle(OWNER_CONTAINER_ID);
|
|
478
|
-
let ports: NodePorts | undefined;
|
|
433
|
+
private announceAddrsFor(containerId: string, ports: NodePorts): string[] {
|
|
434
|
+
if (!this.announceAddrs) return [];
|
|
479
435
|
try {
|
|
480
|
-
|
|
481
|
-
// mapping (external → internal) lands on it — hence the p2p override, which
|
|
482
|
-
// wins over the dropped handle's. Every other port comes back as it was, for
|
|
483
|
-
// the same reason as in createContainer.
|
|
484
|
-
ports = allocateNodePorts(this.portAllocator, { ...reusedNodePorts(dropped), p2p: config.libp2pPort });
|
|
485
|
-
const result = this.launchChild({
|
|
486
|
-
containerId: OWNER_CONTAINER_ID,
|
|
487
|
-
partyId: config.partyId,
|
|
488
|
-
profile,
|
|
489
|
-
ports,
|
|
490
|
-
owner: true,
|
|
491
|
-
buildConfig: (workdir) => this.buildOwnerChildConfig(config, profile, workdir, push),
|
|
492
|
-
extraArgs: ['--owner', '--admin-port', String(ports.admin), '--identity-file', config.identityPath],
|
|
493
|
-
});
|
|
494
|
-
const handle = this.handles.get(result.dockerId)!;
|
|
495
|
-
return toNodeInfo(handle);
|
|
436
|
+
return this.announceAddrs(containerId, ports);
|
|
496
437
|
} catch (err) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
this.restoreDroppedHandles(dropped);
|
|
500
|
-
// Deliberately NOT `discardWorkdir` — the asymmetry with createContainer
|
|
501
|
-
// is the point. `<rootDir>/owner` is a single fixed path reused by every
|
|
502
|
-
// restart and holds the host's own control-DB storage, so it neither
|
|
503
|
-
// grows without bound nor is safe to delete on a failed spawn.
|
|
504
|
-
throw err;
|
|
438
|
+
log('announce hook failed for %s; it starts with no public addresses: %s', containerId, (err as Error).message);
|
|
439
|
+
return [];
|
|
505
440
|
}
|
|
506
441
|
}
|
|
507
442
|
|
|
508
|
-
/** Endpoint + bearer for the owner node's admin channel, if spawned. */
|
|
509
|
-
getOwnerAdminEndpoint(): OwnerAdminEndpoint | undefined {
|
|
510
|
-
const handle = this.findOwnerHandle();
|
|
511
|
-
if (!handle) return undefined;
|
|
512
|
-
return {
|
|
513
|
-
baseUrl: `http://127.0.0.1:${handle.ports.admin}`,
|
|
514
|
-
token: handle.startupToken,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/** Whether spawn parameters for the owner node are known (persisted). */
|
|
519
|
-
hasOwnerConfig(): boolean {
|
|
520
|
-
return this.ownerConfig !== undefined;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/** Whether the given friendly/docker id refers to the owner node. */
|
|
524
|
-
isOwnerNode(idOrDockerId: string): boolean {
|
|
525
|
-
if (idOrDockerId === OWNER_CONTAINER_ID) return true;
|
|
526
|
-
const direct = this.handles.get(idOrDockerId);
|
|
527
|
-
if (direct?.owner) return true;
|
|
528
|
-
for (const h of this.handles.values()) {
|
|
529
|
-
if (h.owner && h.containerId === idOrDockerId) return true;
|
|
530
|
-
}
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/** Stop the owner node (if running). Used on graceful shutdown. */
|
|
535
|
-
async stopOwnerNode(): Promise<void> {
|
|
536
|
-
const handle = this.findOwnerHandle();
|
|
537
|
-
if (handle && isPidAlive(handle.pid)) {
|
|
538
|
-
await this.stopContainer(handle.dockerId);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/** Stop the owner node (if running), then re-spawn it from saved config. */
|
|
543
|
-
async restartOwnerNode(): Promise<ManagedNodeInfo> {
|
|
544
|
-
const handle = this.findOwnerHandle();
|
|
545
|
-
if (handle && isPidAlive(handle.pid)) {
|
|
546
|
-
await this.stopContainer(handle.dockerId);
|
|
547
|
-
}
|
|
548
|
-
return this.ensureOwnerNode();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
443
|
/**
|
|
552
|
-
* Core spawn mechanics
|
|
444
|
+
* Core spawn mechanics behind `createContainer`.
|
|
553
445
|
*
|
|
554
446
|
* The caller has already allocated `ports` and **owns their lifetime**: this
|
|
555
|
-
* method never releases them, so
|
|
447
|
+
* method never releases them, so the caller must release on any throw out
|
|
556
448
|
* of here (it has to unwind its handle drop on the same path anyway — see
|
|
557
449
|
* `restoreDroppedHandles`).
|
|
558
450
|
*
|
|
@@ -568,10 +460,9 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
568
460
|
partyId: string;
|
|
569
461
|
profile: 'storage' | 'transaction';
|
|
570
462
|
ports: NodePorts;
|
|
571
|
-
owner: boolean;
|
|
572
463
|
buildConfig: (workdir: string) => CliConfig;
|
|
573
464
|
extraArgs: string[];
|
|
574
|
-
/** Extra env vars merged into the child's environment (e.g.
|
|
465
|
+
/** Extra env vars merged into the child's environment (e.g. CADRE_CLAIM_SECRET). */
|
|
575
466
|
extraEnv?: Record<string, string>;
|
|
576
467
|
memoryLimit?: string;
|
|
577
468
|
}): OrchestratorCreateResult {
|
|
@@ -599,6 +490,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
599
490
|
const memoryLimit = opts.memoryLimit ?? this.defaultMemoryLimit;
|
|
600
491
|
const heapBytes = parseMemoryLimit(memoryLimit);
|
|
601
492
|
const heapMB = heapBytes !== undefined ? Math.max(64, Math.floor(heapBytes / (1024 * 1024))) : undefined;
|
|
493
|
+
const announcedAddrs = this.announceAddrsFor(containerId, ports);
|
|
602
494
|
|
|
603
495
|
const env: NodeJS.ProcessEnv = {
|
|
604
496
|
// Scrubbed, not raw process.env: every CADRE_* config-override key is
|
|
@@ -612,19 +504,16 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
612
504
|
CADRE_HEALTH_PORT: String(ports.health),
|
|
613
505
|
CADRE_METRICS_PORT: String(ports.metrics),
|
|
614
506
|
CADRE_LISTEN_ADDRS: childListenAddrs(ports).join(','),
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
// WebSocket port, and every strand node are reached through observed addresses only,
|
|
626
|
-
// since no `CADRE_RELAY_ADDRS` is set here and a child therefore holds no relay
|
|
627
|
-
// reservation.
|
|
507
|
+
// The append form, not `CADRE_ANNOUNCE_ADDRS`: that one replaces the advertised
|
|
508
|
+
// set and would drop the LAN addresses a phone at home dials. Every entry must be
|
|
509
|
+
// a valid multiaddr: cadre-core refuses to start on one that is not.
|
|
510
|
+
// The TCP and WebSocket ports are the CHILD'S CONTROL NODE's alone: each strand
|
|
511
|
+
// node the child runs binds the same two entries with OS-assigned ports instead,
|
|
512
|
+
// since one port cannot be held twice, and inherits neither announce field
|
|
513
|
+
// (`cadre-core/src/strand-network-config.ts`). Those ports are not mapped, and
|
|
514
|
+
// strand nodes are reached through observed addresses only, since no
|
|
515
|
+
// `CADRE_RELAY_ADDRS` is set here and a child therefore holds no relay reservation.
|
|
516
|
+
...(announcedAddrs.length > 0 ? { CADRE_APPEND_ANNOUNCE_ADDRS: announcedAddrs.join(',') } : {}),
|
|
628
517
|
CADRE_SEED_TOKEN: seedToken,
|
|
629
518
|
// Pin each child's node-local state (trusted-owner anchor, retained
|
|
630
519
|
// cold-start dial targets) to its OWN workdir. This is the same value the
|
|
@@ -692,7 +581,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
692
581
|
spawnedAt: new Date().toISOString(),
|
|
693
582
|
partyId: opts.partyId,
|
|
694
583
|
profile: opts.profile,
|
|
695
|
-
|
|
584
|
+
announcedAddrs,
|
|
696
585
|
child,
|
|
697
586
|
alive: true,
|
|
698
587
|
};
|
|
@@ -736,7 +625,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
736
625
|
|
|
737
626
|
/**
|
|
738
627
|
* Refuse to re-spawn `containerId` while a previous child of it may still be
|
|
739
|
-
* running.
|
|
628
|
+
* running. `createContainer` calls this as its last `await` before the handle
|
|
740
629
|
* drop, so a refusal happens while nothing has been released, no token rotated
|
|
741
630
|
* and no child launched — the caller sees an ordinary failed spawn.
|
|
742
631
|
*
|
|
@@ -754,9 +643,6 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
754
643
|
* is already bound, refuse. The pid is never killed on that evidence — it may
|
|
755
644
|
* not be ours.
|
|
756
645
|
*
|
|
757
|
-
* `overrides` are the ports the caller will override on top of the reused ones
|
|
758
|
-
* (the owner node's configured p2p port); `owner` adds the admin channel.
|
|
759
|
-
*
|
|
760
646
|
* NOTE: the probe cannot see a re-attached child still loading its modules —
|
|
761
647
|
* `cadre-cli start` writes its token and only then binds, so a child in that
|
|
762
648
|
* window shows neither, and a re-spawn launched then races it for the ports.
|
|
@@ -771,17 +657,13 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
771
657
|
* child is ever seen dying on its own port right after a host restart, probe by
|
|
772
658
|
* connecting instead of binding.
|
|
773
659
|
*/
|
|
774
|
-
private async refuseRespawnOverLiveChild(
|
|
775
|
-
containerId: string,
|
|
776
|
-
overrides: Partial<NodePorts>,
|
|
777
|
-
owner: boolean,
|
|
778
|
-
): Promise<void> {
|
|
660
|
+
private async refuseRespawnOverLiveChild(containerId: string): Promise<void> {
|
|
779
661
|
const previous = this.handlesFor(containerId);
|
|
780
662
|
if (previous.some(isHandleLive)) {
|
|
781
663
|
throw new Error(`container ${containerId} is still running`);
|
|
782
664
|
}
|
|
783
665
|
if (!previous.some((h) => isPidAlive(h.pid))) return;
|
|
784
|
-
const bindings = childPortBindings(
|
|
666
|
+
const bindings = childPortBindings(reusedNodePorts(previous));
|
|
785
667
|
try {
|
|
786
668
|
await Promise.all(bindings.map(assertPortFree));
|
|
787
669
|
} catch (err) {
|
|
@@ -800,9 +682,9 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
800
682
|
/**
|
|
801
683
|
* Drop any handle left over from a previous spawn of the same `containerId`
|
|
802
684
|
* and release its ports. Handles are keyed by the per-spawn `dockerId`, so
|
|
803
|
-
* without this a re-spawn (
|
|
685
|
+
* without this a re-spawn (a hosted node's respawn) would strand the prior
|
|
804
686
|
* handle in the map forever, leaking its ports from a bounded range each
|
|
805
|
-
* time.
|
|
687
|
+
* time.
|
|
806
688
|
*
|
|
807
689
|
* DO NOT delete the workdir: the identity key and node-local stores
|
|
808
690
|
* (trusted-owner anchor, retained cold-start dial targets) live there and are
|
|
@@ -812,16 +694,16 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
812
694
|
* Releasing the ports hands them straight back to the allocator, and the
|
|
813
695
|
* re-spawn then takes those same ports as overrides (`reusedNodePorts`), so a
|
|
814
696
|
* re-spawn while the *previous* child is still listening would bind-clash.
|
|
815
|
-
*
|
|
697
|
+
* The caller must therefore never drop a live child, and does so only after
|
|
816
698
|
* {@link refuseRespawnOverLiveChild} has passed. A caller that needs to replace
|
|
817
699
|
* a live child must stop it first.
|
|
818
700
|
*
|
|
819
701
|
* **The drop is reversible.** It happens before the launch can fail, so the
|
|
820
|
-
* dropped handles are returned and
|
|
702
|
+
* dropped handles are returned and the caller restores them via
|
|
821
703
|
* {@link restoreDroppedHandles} when the launch throws — otherwise the caller
|
|
822
704
|
* would be left holding a dockerId this orchestrator no longer knows, and the
|
|
823
705
|
* node's workdir could never be reclaimed. On a *successful* spawn the drop
|
|
824
|
-
* stands: `
|
|
706
|
+
* stands: `HostedNodeService.abandonRespawn` depends on the old handle being
|
|
825
707
|
* gone by then.
|
|
826
708
|
*/
|
|
827
709
|
private dropStaleHandle(containerId: string): Handle[] {
|
|
@@ -841,10 +723,10 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
841
723
|
*
|
|
842
724
|
* **Precondition: the drop → launch window is synchronous.** Restoring is
|
|
843
725
|
* sound only because nothing can have taken those ports or run cleanup
|
|
844
|
-
* against the handle in between — `launchChild` contains no `await` and
|
|
845
|
-
*
|
|
726
|
+
* against the handle in between — `launchChild` contains no `await` and the
|
|
727
|
+
* caller resolves push credentials *before* its drop. `launchChild`'s half
|
|
846
728
|
* is enforced: its declared return type is not a Promise, so adding an
|
|
847
|
-
* `await` there is a compile error. The
|
|
729
|
+
* `await` there is a compile error. The caller's half is not — an `await`
|
|
848
730
|
* slipped between a drop and its launch silently breaks this.
|
|
849
731
|
*
|
|
850
732
|
* No `persist()` here: `state.json` is rewritten only by `launchChild` and
|
|
@@ -872,14 +754,6 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
872
754
|
return join(this.rootDir, containerId);
|
|
873
755
|
}
|
|
874
756
|
|
|
875
|
-
/** Locate the owner node's handle, if one has been spawned. */
|
|
876
|
-
private findOwnerHandle(): Handle | undefined {
|
|
877
|
-
for (const h of this.handles.values()) {
|
|
878
|
-
if (h.owner || h.containerId === OWNER_CONTAINER_ID) return h;
|
|
879
|
-
}
|
|
880
|
-
return undefined;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
757
|
/**
|
|
884
758
|
* Inverse of {@link releasePorts} — hold a handle's whole port set against
|
|
885
759
|
* the allocator. Used both to rehydrate on `init` and to undo a drop whose
|
|
@@ -963,6 +837,11 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
963
837
|
|
|
964
838
|
this.handles.delete(dockerId);
|
|
965
839
|
this.persist();
|
|
840
|
+
// Removal is a state change too: `NatService` releases the node's port
|
|
841
|
+
// mappings on it at once rather than at its next timer pass. A stopped
|
|
842
|
+
// event for a node no longer in `listNodes()` is how listeners tell a
|
|
843
|
+
// removal from a stop.
|
|
844
|
+
this.emitStateChange(handle);
|
|
966
845
|
}
|
|
967
846
|
|
|
968
847
|
/**
|
|
@@ -1046,51 +925,10 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
1046
925
|
return cfg;
|
|
1047
926
|
}
|
|
1048
927
|
|
|
1049
|
-
/**
|
|
1050
|
-
* Child config for the **host's own personal cadre** owner node — the opt-in
|
|
1051
|
-
* "founder" persona spawned only when `ownCadre.enabled` (see
|
|
1052
|
-
* docs/cadre-host.md § Two roles: donor and founder). It founds/joins the
|
|
1053
|
-
* control network for the host's own `partyId` (no bootstrap peers — it is the
|
|
1054
|
-
* founding node of the host's *own* cadre) and carries the host identity via
|
|
1055
|
-
* `--identity-file` (passed as a spawn arg).
|
|
1056
|
-
*
|
|
1057
|
-
* This is NOT the node cadre-host donates to a requester. Donated nodes are
|
|
1058
|
-
* generic (`createContainer` / `buildChildConfig`): they join the
|
|
1059
|
-
* **requester's** cadre via bootstrap peers and pin the *requester's* owner
|
|
1060
|
-
* key — they never run a host genesis.
|
|
1061
|
-
*/
|
|
1062
|
-
private buildOwnerChildConfig(
|
|
1063
|
-
cfg: OwnerSpawnConfig,
|
|
1064
|
-
profile: 'storage' | 'transaction',
|
|
1065
|
-
workdir: string,
|
|
1066
|
-
push?: PushCredentials,
|
|
1067
|
-
): CliConfig {
|
|
1068
|
-
const config: CliConfig = {
|
|
1069
|
-
controlNetwork: {
|
|
1070
|
-
partyId: cfg.partyId,
|
|
1071
|
-
bootstrapNodes: [],
|
|
1072
|
-
},
|
|
1073
|
-
profile,
|
|
1074
|
-
strandFilter: 'all',
|
|
1075
|
-
};
|
|
1076
|
-
if (profile === 'storage') {
|
|
1077
|
-
config.storage = { type: 'file', path: join(workdir, 'storage') };
|
|
1078
|
-
}
|
|
1079
|
-
// The host-own-cadre owner/storage node participates in strands, so it owns
|
|
1080
|
-
// the push-wake fan-out when credentials are configured. (Donated nodes
|
|
1081
|
-
// belong to foreign cadres and get NO host push block — see the donor path.)
|
|
1082
|
-
// Written into cadre.json (same host trust boundary as the workdir's
|
|
1083
|
-
// control-DB); keys are never logged — only the redacted presence line in
|
|
1084
|
-
// resolvePush.
|
|
1085
|
-
if (push) config.push = push;
|
|
1086
|
-
return config;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
928
|
private persist(): void {
|
|
1090
929
|
const state: PersistedState = {
|
|
1091
930
|
version: 1,
|
|
1092
931
|
handles: [...this.handles.values()].map((h) => this.toPersisted(h)),
|
|
1093
|
-
...(this.ownerConfig ? { ownerConfig: this.ownerConfig } : {}),
|
|
1094
932
|
};
|
|
1095
933
|
this.stateStore.save(state);
|
|
1096
934
|
}
|
|
@@ -1118,7 +956,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
1118
956
|
spawnedAt: h.spawnedAt,
|
|
1119
957
|
partyId: h.partyId,
|
|
1120
958
|
profile: h.profile,
|
|
1121
|
-
|
|
959
|
+
announcedAddrs: h.announcedAddrs,
|
|
1122
960
|
};
|
|
1123
961
|
}
|
|
1124
962
|
}
|
|
@@ -1133,7 +971,7 @@ function toNodeInfo(h: Handle): ManagedNodeInfo {
|
|
|
1133
971
|
spawnedAt: h.spawnedAt,
|
|
1134
972
|
workdir: h.workdir,
|
|
1135
973
|
ports: { ...h.ports },
|
|
1136
|
-
...
|
|
974
|
+
announcedAddrs: [...h.announcedAddrs],
|
|
1137
975
|
};
|
|
1138
976
|
}
|
|
1139
977
|
|
|
@@ -1157,8 +995,8 @@ function resolveCadreCliBin(): string {
|
|
|
1157
995
|
|
|
1158
996
|
/**
|
|
1159
997
|
* Whether the process a handle names is still the child it was launched as — the
|
|
1160
|
-
* one liveness rule behind `init`, `isRunning
|
|
1161
|
-
*
|
|
998
|
+
* one liveness rule behind `init`, `isRunning` and the pre-spawn guard, so they
|
|
999
|
+
* cannot drift apart.
|
|
1162
1000
|
*
|
|
1163
1001
|
* A handle this process spawned carries its `ChildProcess`, and that answer is
|
|
1164
1002
|
* exact from the moment of spawn: no exit seen, and the pid still answering. (The
|