@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.
Files changed (337) hide show
  1. package/README.md +332 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +301 -505
  4. package/dist/bin/host.js.map +1 -1
  5. package/dist/bin/join-output.d.ts +56 -0
  6. package/dist/bin/join-output.js +92 -0
  7. package/dist/bin/join-output.js.map +1 -0
  8. package/dist/bin/nat-output.d.ts +49 -0
  9. package/dist/bin/nat-output.js +127 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/hosted/claim-details.d.ts +44 -0
  12. package/dist/hosted/claim-details.js +66 -0
  13. package/dist/hosted/claim-details.js.map +1 -0
  14. package/dist/hosted/hosted-node-service.d.ts +318 -0
  15. package/dist/hosted/hosted-node-service.js +666 -0
  16. package/dist/hosted/hosted-node-service.js.map +1 -0
  17. package/dist/hosted/hosted-node-store.d.ts +40 -0
  18. package/dist/hosted/hosted-node-store.js +126 -0
  19. package/dist/hosted/hosted-node-store.js.map +1 -0
  20. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  21. package/dist/hosted/hosted-node-supervisor.js +396 -0
  22. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  23. package/dist/hosted/hosted-node-watcher.d.ts +83 -0
  24. package/dist/hosted/hosted-node-watcher.js +224 -0
  25. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  26. package/dist/hosted/index.d.ts +16 -0
  27. package/dist/hosted/index.js +13 -0
  28. package/dist/hosted/index.js.map +1 -0
  29. package/dist/hosted/node-status.d.ts +35 -0
  30. package/dist/hosted/node-status.js +61 -0
  31. package/dist/hosted/node-status.js.map +1 -0
  32. package/dist/hosted/types.d.ts +134 -0
  33. package/dist/hosted/types.js +22 -0
  34. package/dist/hosted/types.js.map +1 -0
  35. package/dist/index.d.ts +12 -24
  36. package/dist/index.js +9 -19
  37. package/dist/index.js.map +1 -1
  38. package/dist/installer/config.d.ts +4 -34
  39. package/dist/installer/config.js +3 -18
  40. package/dist/installer/config.js.map +1 -1
  41. package/dist/installer/index.d.ts +7 -19
  42. package/dist/installer/index.js +14 -69
  43. package/dist/installer/index.js.map +1 -1
  44. package/dist/installer/paths.d.ts +0 -2
  45. package/dist/installer/paths.js +0 -4
  46. package/dist/installer/paths.js.map +1 -1
  47. package/dist/installer/wizard.d.ts +0 -7
  48. package/dist/installer/wizard.js +1 -8
  49. package/dist/installer/wizard.js.map +1 -1
  50. package/dist/nat/address-resolver.d.ts +18 -17
  51. package/dist/nat/address-resolver.js +79 -25
  52. package/dist/nat/address-resolver.js.map +1 -1
  53. package/dist/nat/address-watch.d.ts +37 -0
  54. package/dist/nat/address-watch.js +84 -0
  55. package/dist/nat/address-watch.js.map +1 -0
  56. package/dist/nat/ddns/duckdns.d.ts +2 -1
  57. package/dist/nat/ddns/duckdns.js +5 -1
  58. package/dist/nat/ddns/duckdns.js.map +1 -1
  59. package/dist/nat/ddns/index.d.ts +6 -0
  60. package/dist/nat/ddns/index.js.map +1 -1
  61. package/dist/nat/index.d.ts +12 -9
  62. package/dist/nat/index.js +6 -5
  63. package/dist/nat/index.js.map +1 -1
  64. package/dist/nat/nat-service.d.ts +203 -77
  65. package/dist/nat/nat-service.js +602 -237
  66. package/dist/nat/nat-service.js.map +1 -1
  67. package/dist/nat/nat-store.d.ts +12 -5
  68. package/dist/nat/nat-store.js +114 -30
  69. package/dist/nat/nat-store.js.map +1 -1
  70. package/dist/nat/port-mapper.d.ts +83 -79
  71. package/dist/nat/port-mapper.js +148 -189
  72. package/dist/nat/port-mapper.js.map +1 -1
  73. package/dist/nat/reachability.d.ts +31 -15
  74. package/dist/nat/reachability.js +97 -22
  75. package/dist/nat/reachability.js.map +1 -1
  76. package/dist/nat/types.d.ts +78 -27
  77. package/dist/nat/types.js +8 -5
  78. package/dist/nat/types.js.map +1 -1
  79. package/dist/orchestrator/host-process-orchestrator.d.ts +29 -71
  80. package/dist/orchestrator/host-process-orchestrator.js +78 -218
  81. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  82. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  83. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  84. package/dist/orchestrator/identity-file.js.map +1 -0
  85. package/dist/orchestrator/index.d.ts +2 -3
  86. package/dist/orchestrator/index.js +1 -1
  87. package/dist/orchestrator/index.js.map +1 -1
  88. package/dist/orchestrator/node-identity.d.ts +6 -6
  89. package/dist/orchestrator/node-identity.js +7 -7
  90. package/dist/orchestrator/node-identity.js.map +1 -1
  91. package/dist/orchestrator/port-allocator.d.ts +5 -6
  92. package/dist/orchestrator/port-allocator.js +9 -11
  93. package/dist/orchestrator/port-allocator.js.map +1 -1
  94. package/dist/orchestrator/state-store.d.ts +3 -5
  95. package/dist/orchestrator/state-store.js +2 -10
  96. package/dist/orchestrator/state-store.js.map +1 -1
  97. package/dist/orchestrator/types.d.ts +33 -22
  98. package/dist/orchestrator/types.js.map +1 -1
  99. package/dist/server/error-handler.d.ts +5 -25
  100. package/dist/server/error-handler.js +11 -74
  101. package/dist/server/error-handler.js.map +1 -1
  102. package/dist/server/events/types.d.ts +7 -11
  103. package/dist/server/events/types.js +2 -1
  104. package/dist/server/events/types.js.map +1 -1
  105. package/dist/server/index.d.ts +11 -37
  106. package/dist/server/index.js +30 -48
  107. package/dist/server/index.js.map +1 -1
  108. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  109. package/dist/server/routes/hosted-nodes.js +68 -0
  110. package/dist/server/routes/hosted-nodes.js.map +1 -0
  111. package/dist/server/routes/nat.d.ts +4 -1
  112. package/dist/server/routes/nat.js +13 -2
  113. package/dist/server/routes/nat.js.map +1 -1
  114. package/dist/server/routes/nodes.d.ts +6 -20
  115. package/dist/server/routes/nodes.js +7 -86
  116. package/dist/server/routes/nodes.js.map +1 -1
  117. package/dist/server/routes/settings.d.ts +2 -7
  118. package/dist/server/routes/settings.js +3 -7
  119. package/dist/server/routes/settings.js.map +1 -1
  120. package/dist/server/routes/status.d.ts +2 -25
  121. package/dist/server/routes/status.js +1 -11
  122. package/dist/server/routes/status.js.map +1 -1
  123. package/dist/server/server.js +8 -8
  124. package/dist/server/settings-store.d.ts +2 -2
  125. package/dist/server/settings-store.js +2 -2
  126. package/dist/server/static.js +1 -2
  127. package/dist/server/static.js.map +1 -1
  128. package/dist/ui/assets/index-4FWuU8ye.js +19 -0
  129. package/dist/ui/assets/index-4FWuU8ye.js.map +1 -0
  130. package/dist/ui/assets/index-Di9Q4d01.css +1 -0
  131. package/dist/ui/index.html +2 -2
  132. package/dist/update/index.d.ts +2 -2
  133. package/dist/update/index.js +2 -2
  134. package/dist/update/store.d.ts +1 -1
  135. package/dist/update/store.js +1 -1
  136. package/dist/update/types.d.ts +1 -1
  137. package/dist/update/types.js +1 -1
  138. package/package.json +5 -6
  139. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  140. package/src/__tests__/cli.smoke.test.ts +1 -1
  141. package/src/__tests__/global-setup.ts +2 -3
  142. package/src/__tests__/orchestrator-claim-secret.test.ts +112 -0
  143. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  144. package/src/__tests__/orchestrator-node-identity.test.ts +182 -203
  145. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  146. package/src/__tests__/orchestrator-push.test.ts +138 -146
  147. package/src/__tests__/orchestrator.test.ts +1019 -1063
  148. package/src/bin/host.ts +368 -553
  149. package/src/bin/join-output.ts +120 -0
  150. package/src/bin/nat-output.ts +177 -0
  151. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +173 -173
  152. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +265 -243
  153. package/src/hosted/__tests__/hosted-node-service.test.ts +609 -0
  154. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +474 -0
  155. package/src/hosted/__tests__/test-invitation.ts +33 -0
  156. package/src/hosted/claim-details.ts +96 -0
  157. package/src/hosted/hosted-node-service.ts +795 -0
  158. package/src/hosted/hosted-node-store.ts +144 -0
  159. package/src/hosted/hosted-node-supervisor.ts +439 -0
  160. package/src/hosted/hosted-node-watcher.ts +250 -0
  161. package/src/hosted/index.ts +53 -0
  162. package/src/hosted/node-status.ts +71 -0
  163. package/src/hosted/types.ts +153 -0
  164. package/src/index.ts +72 -91
  165. package/src/installer/__tests__/config.test.ts +15 -40
  166. package/src/installer/__tests__/installer.smoke.test.ts +5 -86
  167. package/src/installer/__tests__/wizard.test.ts +56 -64
  168. package/src/installer/config.ts +135 -175
  169. package/src/installer/index.ts +15 -83
  170. package/src/installer/paths.ts +104 -109
  171. package/src/installer/wizard.ts +87 -103
  172. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  173. package/src/nat/__tests__/nat-service.test.ts +433 -408
  174. package/src/nat/__tests__/nat-store.test.ts +127 -93
  175. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  176. package/src/nat/__tests__/reachability.test.ts +96 -13
  177. package/src/nat/address-resolver.ts +94 -52
  178. package/src/nat/address-watch.ts +92 -0
  179. package/src/nat/ddns/duckdns.ts +5 -1
  180. package/src/nat/ddns/index.ts +6 -0
  181. package/src/nat/index.ts +87 -71
  182. package/src/nat/nat-service.ts +889 -501
  183. package/src/nat/nat-store.ts +237 -154
  184. package/src/nat/port-mapper.ts +241 -329
  185. package/src/nat/reachability.ts +134 -25
  186. package/src/nat/types.ts +209 -159
  187. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  188. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  189. package/src/orchestrator/host-process-orchestrator.ts +81 -243
  190. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  191. package/src/orchestrator/index.ts +9 -10
  192. package/src/orchestrator/node-identity.ts +41 -41
  193. package/src/orchestrator/port-allocator.ts +71 -73
  194. package/src/orchestrator/state-store.ts +76 -90
  195. package/src/orchestrator/types.ts +34 -23
  196. package/src/server/__tests__/bus.test.ts +38 -38
  197. package/src/server/__tests__/error-handler.test.ts +81 -118
  198. package/src/server/__tests__/fakes.ts +40 -44
  199. package/src/server/__tests__/hosted-nodes-route.test.ts +160 -0
  200. package/src/server/__tests__/nodes-route.test.ts +114 -283
  201. package/src/server/__tests__/publishers.test.ts +193 -180
  202. package/src/server/__tests__/server.smoke.test.ts +134 -223
  203. package/src/server/__tests__/settings-route.test.ts +179 -195
  204. package/src/server/__tests__/sse-route.test.ts +119 -119
  205. package/src/server/__tests__/status-route.test.ts +82 -121
  206. package/src/server/error-handler.ts +112 -178
  207. package/src/server/events/types.ts +20 -24
  208. package/src/server/index.ts +223 -267
  209. package/src/server/routes/hosted-nodes.ts +83 -0
  210. package/src/server/routes/nat.ts +75 -63
  211. package/src/server/routes/nodes.ts +96 -188
  212. package/src/server/routes/settings.ts +115 -123
  213. package/src/server/routes/status.ts +102 -134
  214. package/src/server/server.ts +8 -8
  215. package/src/server/settings-store.ts +2 -2
  216. package/src/server/static.ts +1 -2
  217. package/src/update/index.ts +2 -2
  218. package/src/update/store.ts +1 -1
  219. package/src/update/types.ts +1 -1
  220. package/dist/auth/duration.d.ts +0 -1
  221. package/dist/auth/duration.js +0 -32
  222. package/dist/auth/duration.js.map +0 -1
  223. package/dist/auth/index.d.ts +0 -11
  224. package/dist/auth/index.js +0 -9
  225. package/dist/auth/index.js.map +0 -1
  226. package/dist/auth/self-label.d.ts +0 -36
  227. package/dist/auth/self-label.js +0 -57
  228. package/dist/auth/self-label.js.map +0 -1
  229. package/dist/auth/trust-circle-store.d.ts +0 -26
  230. package/dist/auth/trust-circle-store.js +0 -129
  231. package/dist/auth/trust-circle-store.js.map +0 -1
  232. package/dist/auth/trust-circle.d.ts +0 -156
  233. package/dist/auth/trust-circle.js +0 -307
  234. package/dist/auth/trust-circle.js.map +0 -1
  235. package/dist/auth/types.d.ts +0 -84
  236. package/dist/auth/types.js +0 -24
  237. package/dist/auth/types.js.map +0 -1
  238. package/dist/donation/donation-service.d.ts +0 -389
  239. package/dist/donation/donation-service.js +0 -828
  240. package/dist/donation/donation-service.js.map +0 -1
  241. package/dist/donation/donation-store.d.ts +0 -51
  242. package/dist/donation/donation-store.js +0 -152
  243. package/dist/donation/donation-store.js.map +0 -1
  244. package/dist/donation/donation-supervisor.js +0 -364
  245. package/dist/donation/donation-supervisor.js.map +0 -1
  246. package/dist/donation/grant-service.d.ts +0 -80
  247. package/dist/donation/grant-service.js +0 -210
  248. package/dist/donation/grant-service.js.map +0 -1
  249. package/dist/donation/grant-store.d.ts +0 -38
  250. package/dist/donation/grant-store.js +0 -135
  251. package/dist/donation/grant-store.js.map +0 -1
  252. package/dist/donation/index.d.ts +0 -20
  253. package/dist/donation/index.js +0 -17
  254. package/dist/donation/index.js.map +0 -1
  255. package/dist/donation/types.d.ts +0 -224
  256. package/dist/donation/types.js +0 -40
  257. package/dist/donation/types.js.map +0 -1
  258. package/dist/installer/identity.js.map +0 -1
  259. package/dist/owner/index.d.ts +0 -11
  260. package/dist/owner/index.js +0 -11
  261. package/dist/owner/index.js.map +0 -1
  262. package/dist/owner/owner-node-client.d.ts +0 -110
  263. package/dist/owner/owner-node-client.js +0 -184
  264. package/dist/owner/owner-node-client.js.map +0 -1
  265. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  266. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  267. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  268. package/dist/server/routes/grants-admin.d.ts +0 -29
  269. package/dist/server/routes/grants-admin.js +0 -58
  270. package/dist/server/routes/grants-admin.js.map +0 -1
  271. package/dist/server/routes/grants.d.ts +0 -28
  272. package/dist/server/routes/grants.js +0 -165
  273. package/dist/server/routes/grants.js.map +0 -1
  274. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  275. package/dist/server/routes/provision-request-validation.js +0 -97
  276. package/dist/server/routes/provision-request-validation.js.map +0 -1
  277. package/dist/server/routes/strands.d.ts +0 -20
  278. package/dist/server/routes/strands.js +0 -38
  279. package/dist/server/routes/strands.js.map +0 -1
  280. package/dist/server/routes/trust-circle.d.ts +0 -17
  281. package/dist/server/routes/trust-circle.js +0 -35
  282. package/dist/server/routes/trust-circle.js.map +0 -1
  283. package/dist/strands/index.d.ts +0 -7
  284. package/dist/strands/index.js +0 -6
  285. package/dist/strands/index.js.map +0 -1
  286. package/dist/strands/strand-service.d.ts +0 -50
  287. package/dist/strands/strand-service.js +0 -103
  288. package/dist/strands/strand-service.js.map +0 -1
  289. package/dist/strands/types.d.ts +0 -93
  290. package/dist/strands/types.js +0 -23
  291. package/dist/strands/types.js.map +0 -1
  292. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  293. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  294. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  295. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  296. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  297. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  298. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  299. package/src/auth/__tests__/duration.test.ts +0 -28
  300. package/src/auth/__tests__/self-label.test.ts +0 -85
  301. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  302. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  303. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  304. package/src/auth/duration.ts +0 -33
  305. package/src/auth/index.ts +0 -21
  306. package/src/auth/self-label.ts +0 -74
  307. package/src/auth/trust-circle-store.ts +0 -155
  308. package/src/auth/trust-circle.ts +0 -395
  309. package/src/auth/types.ts +0 -99
  310. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  311. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  312. package/src/donation/__tests__/grant-service.test.ts +0 -213
  313. package/src/donation/__tests__/grant-store.test.ts +0 -113
  314. package/src/donation/donation-service.ts +0 -1033
  315. package/src/donation/donation-store.ts +0 -176
  316. package/src/donation/donation-supervisor.ts +0 -411
  317. package/src/donation/grant-service.ts +0 -252
  318. package/src/donation/grant-store.ts +0 -152
  319. package/src/donation/index.ts +0 -65
  320. package/src/donation/types.ts +0 -260
  321. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  322. package/src/owner/index.ts +0 -12
  323. package/src/owner/owner-node-client.ts +0 -259
  324. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  325. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  326. package/src/server/__tests__/grants-route.test.ts +0 -394
  327. package/src/server/__tests__/strands-route.test.ts +0 -234
  328. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  329. package/src/server/routes/grants-admin.ts +0 -74
  330. package/src/server/routes/grants.ts +0 -193
  331. package/src/server/routes/provision-request-validation.ts +0 -105
  332. package/src/server/routes/strands.ts +0 -54
  333. package/src/server/routes/trust-circle.ts +0 -49
  334. package/src/strands/__tests__/strand-service.test.ts +0 -165
  335. package/src/strands/index.ts +0 -14
  336. package/src/strands/strand-service.ts +0 -135
  337. 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, plus the loopback admin channel —
107
- * for the owner node only, the one child started with `--admin-port`. A key the
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>, owner: boolean): PortBinding[] {
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
- ...(persisted.owner ? { owner: true } : {}),
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). Returns an unsubscribe fn.
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 generic managed node — the donated-node path (the requester's cadre,
268
- * pinned to the *requester's* owner key). The node is given its own protobuf
269
- * identity key inside its workdir (`ensureNodeIdentity`, reused across
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
- * Terminating the loan (`removeContainer`) deletes the workdir, so the key and
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: OrchestratorCreateRequest): Promise<OrchestratorCreateResult> {
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' && !foreignParty ? await this.resolvePush() : undefined;
310
- // Pinned owner key(s) reach the child via CADRE_OWNER_KEYS (comma-separated);
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, {}, false);
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 donation record that
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: donation ids are `grn_<base64url>` and so can
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
- * Ensure the admin's owner node is running, spawning it if absent or
440
- * dead. Idempotent: a second call with the node already alive returns the
441
- * existing handle without launching a second child. The spawn parameters
442
- * are persisted so a later `/api/nodes/:id/{start,restart}` (or an
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
- async ensureOwnerNode(cfg?: OwnerSpawnConfig): Promise<ManagedNodeInfo> {
451
- const config = cfg ?? this.ownerConfig;
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
- // The owner node must listen on the configured libp2p port so the NAT
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
- // Release before restoring — see the same note in createContainer.
498
- if (ports) this.releasePorts(ports);
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 shared by `createContainer` and `ensureOwnerNode`.
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 every caller must release on any throw out
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. CADRE_OWNER_KEYS). */
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
- // NOTE: the scrub above drops every CADRE_* var and `extraEnv` is built only from
616
- // pinnedOwnerKeys, so a managed child advertises only the ports assigned to it here
617
- // — `CADRE_ANNOUNCE_ADDRS`/`CADRE_APPEND_ANNOUNCE_ADDRS` cannot reach it. Fine while
618
- // children are reached at those ports; if a host is ever fronted by a proxy or DNS
619
- // name, plumb an announce var through from host config. The TCP
620
- // and WebSocket ports are the CHILD'S CONTROL NODE's alone: each strand node the
621
- // child runs binds the same two entries with OS-assigned ports instead, since one
622
- // port cannot be held twice (`cadre-core/src/strand-network-config.ts`). A NAT
623
- // forward (the owner node's, via `NatService`) covers the TCP port only, so it
624
- // reaches the control node over TCP alone; a phone outside the LAN, which needs the
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
- ...(opts.owner ? { owner: true } : {}),
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. Both spawn paths call this as their last `await` before the handle
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({ ...reusedNodePorts(previous), ...overrides }, owner);
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 (donated-node respawn) would strand the prior
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. Mirrors the same cleanup in `ensureOwnerNode`.
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
- * Callers must therefore never drop a live child, and both do so only after
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 every caller restores them via
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: `DonationService.abandonRespawn` depends on the old handle being
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 both
845
- * callers resolve push credentials *before* their drop. `launchChild`'s half
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 callers' half is not — an `await`
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
- ...(h.owner ? { owner: true } : {}),
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
- ...(h.owner ? { owner: true } : {}),
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`, `ensureOwnerNode`'s short-circuit
1161
- * and the pre-spawn guard, so they cannot drift apart.
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